buf-fs 0.1.3

A buffer based, in-memory filesystem.
Documentation
#![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};

/// A FAT16, virtual filesystem.
///
/// FAT-16 *IS NOT* case sensitive. All the paths will be silently converted to uppercase, as in
/// any FAT system. It's a TODO to support ext4.
pub struct FileSystem {
    dev: Device,
    cwd: RelativePathBuf,
}

impl From<Device> for FileSystem {
    fn from(dev: Device) -> Self {
        Self {
            dev,
            cwd: RelativePathBuf::from("/"),
        }
    }
}

impl FileSystem {
    /// Creates a new filesystem with the provided partition size.
    ///
    /// The memory buffer of the filesystem is lazy loaded; that is, it will allocate bytes as they
    /// are consumed.
    pub fn new(size: usize) -> anyhow::Result<Self> {
        Device::new(size).map(Self::from)
    }

    /// Serializes the structure into bytes.
    pub fn try_to_bytes(&self) -> anyhow::Result<Vec<u8>> {
        self.dev.try_to_bytes()
    }

    /// Deserializes the structure from bytes.
    pub fn try_from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
        Device::try_from_bytes(bytes).map(Self::from)
    }

    /// Serializes the raw device (MBR + FAT16) into bytes.
    ///
    /// Note: this is a compatible filesystem that can be mounted elsewhere.
    pub fn try_to_raw_device(&self) -> anyhow::Result<Vec<u8>> {
        self.dev.try_to_raw_bytes()
    }

    /// Deserializes the raw device (MBR + FAT16) from bytes.
    pub fn from_raw_device_unchecked(device: Vec<u8>) -> Self {
        Device::from_raw_bytes_unchecked(device).into()
    }

    /// Returns the current work directory.
    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)
    }

    /// Navigates into the provided directory.
    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(())
    }

    /// List the contents of the path.
    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)
    }

    /// Creates the provided path recursively from the current directory.
    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(())
    }

    /// Removes a file or an empty directory.
    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()
        {
            // TODO
            // https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/186
            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(())
    }

    /// Opens the file at the specified path.
    ///
    /// If the file doesn't exist, the flag `File.new` will be set to `true`.
    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))
    }

    /// Saves the file into the buffer.
    ///
    /// Creates the path recursively, if it doesn't exist.
    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));
    }
}