#![doc = include_str!("../README.md")]
#![cfg_attr(not(test), no_std)]
#![warn(missing_docs)]
extern crate alloc;
mod device;
mod mbr;
mod types;
pub use device::*;
pub use types::*;
#[cfg(test)]
mod tests;
use alloc::{string::ToString as _, vec, vec::Vec};
use embedded_sdmmc::{Mode, VolumeIdx};
use relative_path::{Component, RelativePath, RelativePathBuf};
pub struct FileSystem {
dev: Device,
cwd: RelativePathBuf,
}
impl From<Device> for FileSystem {
fn from(dev: Device) -> Self {
Self {
dev,
cwd: RelativePathBuf::from("/"),
}
}
}
impl FileSystem {
pub fn new(size: usize) -> anyhow::Result<Self> {
Device::new(size).map(Self::from)
}
pub fn try_to_bytes(&self) -> anyhow::Result<Vec<u8>> {
self.dev.try_to_bytes()
}
pub fn try_from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
Device::try_from_bytes(bytes).map(Self::from)
}
pub fn try_to_raw_device(&self) -> anyhow::Result<Vec<u8>> {
self.dev.try_to_raw_bytes()
}
pub fn from_raw_device_unchecked(device: Vec<u8>) -> Self {
Device::from_raw_bytes_unchecked(device).into()
}
pub fn cwd(&self) -> &RelativePath {
&self.cwd
}
fn path<P: AsRef<RelativePath>>(&self, path: P) -> RelativePathBuf {
let path = if path.as_ref().as_str().starts_with("/") {
path.as_ref().to_relative_path_buf()
} else {
self.cwd.join(path)
};
normalize_path(path)
}
pub fn cd<P: AsRef<RelativePath>>(&mut self, dir: P) -> anyhow::Result<()> {
let cwd = self.path(dir);
let mut mgr = self.dev.clone().open();
let mut vol = mgr
.open_volume(VolumeIdx(0))
.map_err(|_| anyhow::anyhow!("failed to open volume"))?;
let mut root = vol
.open_root_dir()
.map_err(|_| anyhow::anyhow!("failed to open root dir"))?;
for d in cwd.into_iter() {
root.change_dir(d)
.map_err(|_| anyhow::anyhow!("failed to cd into `{d}`"))?;
}
self.cwd = cwd;
Ok(())
}
pub fn ls<P: AsRef<RelativePath>>(&self, path: P) -> anyhow::Result<Vec<DirOrFile>> {
let cwd = self.path(path);
let mut mgr = self.dev.clone().open();
let mut vol = mgr
.open_volume(VolumeIdx(0))
.map_err(|_| anyhow::anyhow!("failed to open volume"))?;
let mut root = vol
.open_root_dir()
.map_err(|_| anyhow::anyhow!("failed to open root dir"))?;
for d in cwd.into_iter() {
if root.change_dir(d).is_err() {
let file = root
.open_file_in_dir(d, Mode::ReadOnly)
.map_err(|_| anyhow::anyhow!("failed to open file `{d}`"))?;
let file = FilePath::new(cwd, file.length() as usize);
return Ok(vec![file.into()]);
}
}
let mut contents = Vec::with_capacity(20);
root.iterate_dir(|entry| {
let path = entry.name.to_string();
let path = RelativePathBuf::from(path);
if entry.attributes.is_directory() {
if path.as_str() != "." && path.as_str() != ".." {
contents.push(Dir::new(path).into());
}
} else {
contents.push(FilePath::new(path, entry.size as usize).into())
}
})
.map_err(|_| anyhow::anyhow!("failed to iterate directory {cwd}"))?;
contents.sort();
Ok(contents)
}
pub fn mkdir<P: AsRef<RelativePath>>(&mut self, dir: P) -> anyhow::Result<()> {
let cwd = self.path(dir);
let mut mgr = self.dev.clone().open();
let mut vol = mgr
.open_volume(VolumeIdx(0))
.map_err(|_| anyhow::anyhow!("failed to open volume"))?;
let mut root = vol
.open_root_dir()
.map_err(|_| anyhow::anyhow!("failed to open root dir"))?;
for d in cwd.into_iter() {
if root.change_dir(d).is_err() {
root.make_dir_in_dir(d)
.map_err(|_| anyhow::anyhow!("failed to mkdir `{d}` in `{cwd}`"))?;
root.change_dir(d)
.map_err(|_| anyhow::anyhow!("failed to cd into `{d}`"))?;
}
}
Ok(())
}
pub fn rm<P: AsRef<RelativePath>>(&mut self, path: P) -> anyhow::Result<()> {
let mut cwd = self.path(path);
let name = cwd
.file_name()
.ok_or_else(|| anyhow::anyhow!("failed to extract the base name from `{cwd}`."))?
.to_string();
anyhow::ensure!(cwd.pop(), "failed to extract parent from {cwd}.");
let mut mgr = self.dev.clone().open();
let mut vol = mgr
.open_volume(VolumeIdx(0))
.map_err(|_| anyhow::anyhow!("failed to open volume"))?;
let mut root = vol
.open_root_dir()
.map_err(|_| anyhow::anyhow!("failed to open root dir"))?;
for d in cwd.into_iter() {
root.change_dir(d)
.map_err(|_| anyhow::anyhow!("failed to cd into `{d}`"))?;
}
if root
.find_directory_entry(name.as_str())
.map_err(|_| anyhow::anyhow!("failed to read `{name}` from `{cwd}`"))?
.attributes
.is_directory()
{
anyhow::bail!("rmdir is currently not implemented");
}
root.delete_file_in_dir(name.as_str())
.map_err(|_| anyhow::anyhow!("failed to rm `{name}` from `{cwd}`"))?;
Ok(())
}
pub fn open<P: AsRef<RelativePath>>(&mut self, path: P) -> anyhow::Result<File> {
let path = self.path(path);
let name = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("failed to define file name from `{path}`."))?;
let parent = normalize_parent(path.clone());
let mut mgr = self.dev.clone().open();
let mut vol = mgr
.open_volume(VolumeIdx(0))
.map_err(|_| anyhow::anyhow!("failed to open volume"))?;
let mut root = vol
.open_root_dir()
.map_err(|_| anyhow::anyhow!("failed to open root dir"))?;
for d in parent.into_iter() {
match root.change_dir(d) {
Ok(d) => d,
Err(_) => {
let new = true;
let contents = vec![];
return Ok(File::new(path, contents, new));
}
}
}
let mut new = false;
let contents = match root.open_file_in_dir(name, Mode::ReadOnly) {
Ok(mut f) => {
let mut len = f.length() as usize;
let mut contents = vec![0u8; len];
let mut ofs = 0;
while len > 0 {
let n = f
.read(&mut contents[ofs..])
.map_err(|_| anyhow::anyhow!("failed to read from `{path}`/{name}"))?;
len = len.saturating_sub(n);
ofs = ofs.saturating_add(n);
}
contents
}
Err(_) => {
new = true;
vec![]
}
};
Ok(File::new(path, contents, new))
}
pub fn save(&mut self, file: File) -> anyhow::Result<()> {
let path = self.path(&file.path);
let name = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("failed to define file name from `{path}`."))?;
let parent = normalize_parent(path.clone());
let mut mgr = self.dev.clone().open();
let mut vol = mgr
.open_volume(VolumeIdx(0))
.map_err(|_| anyhow::anyhow!("failed to open volume"))?;
let mut root = vol
.open_root_dir()
.map_err(|_| anyhow::anyhow!("failed to open root dir"))?;
for d in parent.into_iter() {
if root.change_dir(d).is_err() {
root.make_dir_in_dir(d)
.map_err(|_| anyhow::anyhow!("failed to mkdir `{d}` in `{path}`"))?;
root.change_dir(d)
.map_err(|_| anyhow::anyhow!("failed to cd into `{d}`"))?;
}
}
root.open_file_in_dir(name, Mode::ReadWriteCreateOrTruncate)
.and_then(|mut f| f.write(&file.contents))
.map_err(|_| anyhow::anyhow!("failed to write to `{path}`/{name}"))?;
Ok(())
}
}
fn normalize_path<P: AsRef<RelativePath>>(path: P) -> RelativePathBuf {
let mut absolute = RelativePathBuf::from("/");
for c in path.as_ref().components() {
match c {
Component::CurDir => (),
Component::ParentDir if absolute == "/" => (),
Component::ParentDir => {
absolute = absolute
.parent()
.map(RelativePath::to_relative_path_buf)
.unwrap_or(absolute);
}
Component::Normal(p) => absolute = absolute.join(p.to_uppercase()),
}
}
absolute
}
fn normalize_parent<P: AsRef<RelativePath>>(path: P) -> RelativePathBuf {
let path = path
.as_ref()
.normalize()
.parent()
.filter(|p| !p.as_str().is_empty())
.map(|p| p.to_relative_path_buf())
.unwrap_or_else(|| RelativePathBuf::from("/"))
.normalize();
match path.components().next() {
Some(Component::Normal("/")) => path,
Some(Component::Normal(_)) => RelativePathBuf::from("/").join(path),
_ => RelativePathBuf::from("/"),
}
}
#[test]
fn normalize_path_works() {
let cases = vec![
("/", "/"),
(".", "/"),
("./", "/"),
("..", "/"),
("../", "/"),
("../..", "/"),
("./../..", "/"),
("/root", "/ROOT"),
];
for (i, o) in cases {
let i = RelativePathBuf::from(i);
let i = normalize_path(i);
assert_eq!(i.as_str(), o);
assert_eq!(i, RelativePathBuf::from(o));
}
}
#[test]
fn normalize_parent_works() {
let cases = vec![
("/", "/"),
(".", "/"),
("./", "/"),
("..", "/"),
("../", "/"),
("../..", "/"),
("./../..", "/"),
("/root", "/"),
("/root/etc", "/root"),
("/root/etc/", "/root"),
("/root/etc/.", "/root"),
];
for (i, o) in cases {
let i = RelativePathBuf::from(i);
let i = normalize_parent(i);
assert_eq!(i.as_str(), o);
assert_eq!(i, RelativePathBuf::from(o));
}
}