diff --git a/CHANGELOG.md b/CHANGELOG.md index d9bcb2d..a8aa658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- New examples, `append_file`, `create_file`, `delete_file`, `list_dir` +- New examples, `append_file`, `create_file`, `delete_file`, `list_dir`, `shell` - New test cases `tests/directories.rs`, `tests/read_file.rs` ### Removed diff --git a/examples/linux/mod.rs b/examples/linux/mod.rs index 1a1b190..5abb99f 100644 --- a/examples/linux/mod.rs +++ b/examples/linux/mod.rs @@ -74,6 +74,7 @@ impl BlockDevice for LinuxBlockDevice { } } +#[derive(Debug)] pub struct Clock; impl TimeSource for Clock { diff --git a/examples/shell.rs b/examples/shell.rs new file mode 100644 index 0000000..35e9e13 --- /dev/null +++ b/examples/shell.rs @@ -0,0 +1,252 @@ +//! A simple shell demo for embedded-sdmmc +//! +//! Presents a basic command prompt which implements some basic MS-DOS style shell commands. + +use std::io::prelude::*; + +use embedded_sdmmc::{Directory, Error, Volume, VolumeIdx, VolumeManager}; + +use crate::linux::{Clock, LinuxBlockDevice}; + +mod linux; + +struct VolumeState { + directory: Directory, + volume: Volume, + path: Vec, +} + +struct Context { + volume_mgr: VolumeManager, + volumes: [Option; 4], + current_volume: usize, +} + +impl Context { + fn current_path(&self) -> Vec { + let Some(s) = &self.volumes[self.current_volume] else { + return vec![]; + }; + 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!("\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} {} {:?}", + entry.name, entry.size, entry.mtime, entry.attributes + ); + })?; + } else if let Some(arg) = line.strip_prefix("cd ") { + 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()); + } + } else if let Some(arg) = line.strip_prefix("cat ") { + let Some(s) = &mut self.volumes[self.current_volume] else { + println!("This volume isn't available"); + return Ok(()); + }; + let f = self.volume_mgr.open_file_in_dir( + s.directory, + arg, + embedded_sdmmc::Mode::ReadOnly, + )?; + let mut inner = || -> Result<(), Error> { + let mut data = Vec::new(); + while !self.volume_mgr.file_eof(f)? { + let mut buffer = vec![0u8; 65536]; + let n = self.volume_mgr.read(f, &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(()) + }; + let r = inner(); + self.volume_mgr.close_file(f)?; + r?; + } else if let Some(arg) = line.strip_prefix("hexdump ") { + let Some(s) = &mut self.volumes[self.current_volume] else { + println!("This volume isn't available"); + return Ok(()); + }; + let f = self.volume_mgr.open_file_in_dir( + s.directory, + arg, + embedded_sdmmc::Mode::ReadOnly, + )?; + let mut inner = || -> Result<(), Error> { + let mut data = Vec::new(); + while !self.volume_mgr.file_eof(f)? { + let mut buffer = vec![0u8; 65536]; + let n = self.volume_mgr.read(f, &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 _padding in 0..(16 - chunk.len()) { + print!(" "); + } + print!("| "); + for b in chunk { + print!( + "{}", + if b.is_ascii_graphic() { + *b as char + } else { + '.' + } + ); + } + println!(); + } + Ok(()) + }; + let r = inner(); + self.volume_mgr.close_file(f)?; + r?; + } else { + println!("Unknown command {line:?} - try 'help' for help"); + } + Ok(()) + } +} + +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()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + println!("Opening '{filename}'..."); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let stdin = std::io::stdin(); + + let mut ctx = Context { + volume_mgr: VolumeManager::new_with_limits(lbd, Clock, 100), + volumes: [None, None, None, None], + current_volume: 0, + }; + + let mut current_volume = None; + for volume_no in 0..4 { + match ctx.volume_mgr.open_volume(VolumeIdx(volume_no)) { + Ok(volume) => { + println!("Volume # {}: found", volume_no,); + match ctx.volume_mgr.open_root_dir(volume) { + Ok(root_dir) => { + ctx.volumes[volume_no] = Some(VolumeState { + directory: root_dir, + volume, + path: vec![], + }); + if current_volume.is_none() { + current_volume = Some(volume_no); + } + } + Err(e) => { + println!("Failed to open root directory: {e:?}"); + ctx.volume_mgr.close_volume(volume).expect("close volume"); + } + } + } + Err(e) => { + println!("Failed to open volume {volume_no}: {e:?}"); + } + } + } + + match current_volume { + Some(n) => { + // Default to the first valid partition + ctx.current_volume = n; + } + None => { + println!("No volumes found in file. Sorry."); + return Ok(()); + } + }; + + loop { + print!("{}:/", ctx.current_volume); + print!("{}", ctx.current_path().join("/")); + print!("> "); + std::io::stdout().flush().unwrap(); + let mut line = String::new(); + stdin.read_line(&mut line)?; + let line = line.trim(); + if line == "quit" { + break; + } else if let Err(e) = ctx.process_line(line) { + println!("Error: {:?}", e); + } + } + + for (idx, s) in ctx.volumes.into_iter().enumerate() { + if let Some(state) = s { + println!("Closing current dir for {idx}..."); + let r = ctx.volume_mgr.close_dir(state.directory); + if let Err(e) = r { + println!("Error closing directory: {e:?}"); + } + println!("Unmounting {idx}..."); + let r = ctx.volume_mgr.close_volume(state.volume); + if let Err(e) = r { + println!("Error closing volume: {e:?}"); + } + } + } + + println!("Bye!"); + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/ondiskdirentry.rs b/src/fat/ondiskdirentry.rs index 8ce3f63..14be74c 100644 --- a/src/fat/ondiskdirentry.rs +++ b/src/fat/ondiskdirentry.rs @@ -148,17 +148,26 @@ impl<'a> OnDiskDirEntry<'a> { entry_block: BlockIdx, entry_offset: u32, ) -> DirEntry { + let attributes = Attributes::create_from_fat(self.raw_attr()); let mut result = DirEntry { name: ShortFileName { contents: [0u8; 11], }, mtime: Timestamp::from_fat(self.write_date(), self.write_time()), ctime: Timestamp::from_fat(self.create_date(), self.create_time()), - attributes: Attributes::create_from_fat(self.raw_attr()), - cluster: if fat_type == FatType::Fat32 { - self.first_cluster_fat32() - } else { - self.first_cluster_fat16() + attributes, + cluster: { + let cluster = if fat_type == FatType::Fat32 { + self.first_cluster_fat32() + } else { + self.first_cluster_fat16() + }; + if cluster == ClusterId::EMPTY && attributes.is_directory() { + // FAT16/FAT32 uses a cluster ID of `0` in the ".." entry to mean 'root directory' + ClusterId::ROOT_DIR + } else { + cluster + } }, size: self.file_size(), entry_block, diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index 096e99c..120eaa5 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -85,6 +85,12 @@ impl ShortFileName { let mut sfn = ShortFileName { contents: [b' '; Self::FILENAME_MAX_LEN], }; + + // Special case `..`, which means "parent directory". + if name == ".." { + return Ok(ShortFileName::parent_dir()); + } + let mut idx = 0; let mut seen_dot = false; for ch in name.bytes() { diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs index f4be611..30c1018 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/search_id.rs @@ -12,6 +12,7 @@ pub struct SearchId(pub(crate) u32); /// Well, it will wrap after `2**32` IDs. But most systems won't open that many /// files, and if they do, they are unlikely to hold one file open and then /// open/close `2**32 - 1` others. +#[derive(Debug)] pub struct SearchIdGenerator { next_id: Wrapping, } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 4cafafa..5abfcbb 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -20,6 +20,7 @@ use heapless::Vec; /// A `VolumeManager` wraps a block device and gives access to the FAT-formatted /// volumes within it. +#[derive(Debug)] pub struct VolumeManager< D, T, @@ -242,6 +243,8 @@ where } }; + debug!("Found dir entry: {:?}", dir_entry); + if !dir_entry.attributes.is_directory() { return Err(Error::OpenedFileAsDir); } @@ -482,7 +485,7 @@ where // Check if it's open already if let Some(dir_entry) = &dir_entry { - if self.file_is_open(volume_info.volume_id, &dir_entry) { + if self.file_is_open(volume_info.volume_id, dir_entry) { return Err(Error::FileAlreadyOpen); } }