use std::{cell::RefCell, io::prelude::*};
use embedded_sdmmc::{
Error as EsError, LfnBuffer, Mode, RawDirectory, RawVolume, ShortFileName, VolumeIdx,
};
type VolumeManager = embedded_sdmmc::VolumeManager<LinuxBlockDevice, Clock, 8, 8, 4>;
type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 8, 4>;
use crate::linux::{Clock, LinuxBlockDevice};
type Error = EsError<std::io::Error>;
mod linux;
#[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 {
fn new<S: AsRef<str> + ?Sized>(s: &S) -> &Path {
unsafe { &*(s.as_ref() as *const str as *const Path) }
}
fn volume(&self) -> Option<char> {
let mut char_iter = self.chars();
match (char_iter.next(), char_iter.next()) {
(Some(volume), Some(':')) => Some(volume),
_ => None,
}
}
fn is_absolute(&self) -> bool {
let tail = self.without_volume();
tail.starts_with('/')
}
fn iterate_dirs(&self) -> impl Iterator<Item = &str> {
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('/')
}
}
fn iterate_components(&self) -> impl Iterator<Item = &str> {
let path = self.without_volume();
let path = path.strip_prefix('/').unwrap_or(path);
path.split('/')
}
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)
}
}
fn without_volume(&self) -> &Path {
if let Some((volume, tail)) = self.split_once(':') {
if volume.chars().count() == 1 {
return Path::new(tail);
}
}
self
}
}
impl PartialEq<str> for Path {
fn eq(&self, other: &str) -> bool {
let s: &str = self;
s == other
}
}
struct VolumeState {
directory: RawDirectory,
volume: RawVolume,
path: Vec<String>,
}
struct Context {
volume_mgr: VolumeManager,
volumes: RefCell<[Option<VolumeState>; 4]>,
current_volume: usize,
}
impl Context {
fn current_path(&self) -> Vec<String> {
let Some(s) = &self.volumes.borrow()[self.current_volume] else {
return vec![];
};
s.path.clone()
}
fn help(&self) -> Result<(), Error> {
println!("Commands:");
println!("\thelp -> this help text");
println!("\t<volume>: -> change volume/partition");
println!("\tstat -> print volume manager status");
println!("\tdir [<path>] -> do a directory listing");
println!("\ttree [<path>] -> do a recursive directory listing");
println!("\tcd .. -> go up a level");
println!("\tcd <path> -> change into directory <path>");
println!("\tcat <path> -> print a text file");
println!("\thexdump <path> -> print a binary file");
println!("\tmkdir <path> -> 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(())
}
fn stat(&self) -> Result<(), Error> {
println!("Status:\n{:#?}", self.volume_mgr);
Ok(())
}
fn dir(&self, path: &Path) -> Result<(), Error> {
println!("Directory listing of {:?}", path);
let dir = self.resolve_existing_directory(path)?;
let mut storage = [0u8; 128];
let mut lfn_buffer = LfnBuffer::new(&mut storage);
dir.iterate_dir_lfn(&mut lfn_buffer, |entry, lfn| {
if !entry.attributes.is_volume() {
print!(
"{:12} {:9} {} {} {:08X?} {:5?}",
entry.name,
entry.size,
entry.ctime,
entry.mtime,
entry.cluster,
entry.attributes,
);
if let Some(lfn) = lfn {
println!(" {:?}", lfn);
} else {
println!();
}
}
})?;
Ok(())
}
fn tree(&self, path: &Path) -> Result<(), Error> {
println!("Directory listing of {:?}", path);
let dir = self.resolve_existing_directory(path)?;
Self::tree_dir(dir)
}
fn tree_dir(dir: Directory) -> Result<(), Error> {
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());
}
})?;
for child in children {
println!("Entering {}", child);
let child_dir = dir.open_dir(&child)?;
Self::tree_dir(child_dir)?;
println!("Returning from {}", child);
}
Ok(())
}
fn cd(&self, full_path: &Path) -> Result<(), Error> {
let volume_idx = self.resolve_volume(full_path)?;
let (mut d, fragment) = self.resolve_filename(full_path)?;
d.change_dir(fragment)?;
let Some(s) = &mut self.volumes.borrow_mut()[volume_idx] else {
return Err(Error::NoSuchVolume);
};
self.volume_mgr
.close_dir(s.directory)
.expect("close open dir");
s.directory = d.to_raw_directory();
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 if fragment == "." {
} else {
s.path.push(fragment.to_owned());
}
}
Ok(())
}
fn cat(&self, filename: &Path) -> Result<(), Error> {
let (dir, filename) = self.resolve_filename(filename)?;
let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?;
let mut data = Vec::new();
while !f.is_eof() {
let mut buffer = vec![0u8; 65536];
let n = f.read(&mut buffer)?;
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(())
}
fn hexdump(&self, filename: &Path) -> Result<(), Error> {
let (dir, filename) = self.resolve_filename(filename)?;
let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?;
let mut data = Vec::new();
while !f.is_eof() {
let mut buffer = vec![0u8; 65536];
let n = f.read(&mut buffer)?;
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(())
}
fn mkdir(&self, dir_name: &Path) -> Result<(), Error> {
let (dir, filename) = self.resolve_filename(dir_name)?;
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(())
}
fn resolve_existing_directory<'a>(&'a self, full_path: &Path) -> Result<Directory<'a>, Error> {
let (mut dir, fragment) = self.resolve_filename(full_path)?;
dir.change_dir(fragment)?;
Ok(dir)
}
fn resolve_volume(&self, path: &Path) -> Result<usize, Error> {
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),
}
}
fn resolve_filename<'a, 'path>(
&'a self,
full_path: &'path Path,
) -> Result<(Directory<'a>, &'path str), Error> {
let volume_idx = self.resolve_volume(full_path)?;
let Some(s) = &self.volumes.borrow()[volume_idx] else {
return Err(Error::NoSuchVolume);
};
let mut work_dir = if full_path.is_absolute() {
self.volume_mgr
.open_root_dir(s.volume)?
.to_directory(&self.volume_mgr)
} else {
self.volume_mgr
.open_dir(s.directory, ".")?
.to_directory(&self.volume_mgr)
};
for fragment in full_path.iterate_dirs() {
work_dir.change_dir(fragment)?;
}
Ok((work_dir, full_path.basename().unwrap_or(".")))
}
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 {
fn drop(&mut self) {
for v in self.volumes.borrow_mut().iter_mut() {
if let Some(v) = v {
println!("Closing directory {:?}", v.directory);
self.volume_mgr
.close_dir(v.directory)
.expect("Closing directory");
println!("Closing volume {:?}", v.volume);
self.volume_mgr
.close_volume(v.volume)
.expect("Closing volume");
}
*v = None;
}
}
}
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: RefCell::new([None, None, None, None]),
current_volume: 0,
};
let mut current_volume = None;
for volume_no in 0..4 {
match ctx.volume_mgr.open_raw_volume(VolumeIdx(volume_no)) {
Ok(volume) => {
println!(
"Volume # {}: found, label: {:?}",
Context::volume_to_letter(volume_no),
ctx.volume_mgr.get_root_volume_label(volume)?
);
match ctx.volume_mgr.open_root_dir(volume) {
Ok(root_dir) => {
ctx.volumes.borrow_mut()[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) => {
ctx.current_volume = n;
}
None => {
println!("No volumes found in file. Sorry.");
return Ok(());
}
};
loop {
print!("{}:/", Context::volume_to_letter(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);
}
}
println!("Bye!");
Ok(())
}