liteboxfs 0.1.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::{
    collections::{HashMap, HashSet},
    path::{Path, PathBuf},
};

use fuser::{Generation, INodeNo};

use super::pool::IdPool;
use crate::FileId;
use std::collections::hash_map::Entry;

/// A table for allocating inodes in a virtual file system.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct InodeTable {
    /// A pool of unique integers to act as inodes.
    pool: IdPool,

    /// A map of Unix inode numbers to their associated liteboxfs file IDs.
    files_by_inode: HashMap<INodeNo, FileId>,

    /// A map of liteboxfs file IDs to their associated Unix inode numbers.
    inodes_by_file: HashMap<FileId, INodeNo>,

    /// A map of inodes to the set of paths which refer to the file.
    paths: HashMap<INodeNo, HashSet<PathBuf>>,

    /// A map of inode numbers to their generations.
    ///
    /// Generations are a concept in libfuse in which an additional integer ID is associated with
    /// each inode to ensure they're unique even when the inode values are reused.
    ///
    /// If an inode is not in this map, its generation is `0`.
    generations: HashMap<INodeNo, Generation>,
}

impl InodeTable {
    pub fn new(root: &Path) -> Self {
        let mut table = Self {
            pool: IdPool::new([INodeNo::ROOT.0]),
            files_by_inode: HashMap::new(),
            inodes_by_file: HashMap::new(),
            paths: HashMap::new(),
            generations: HashMap::new(),
        };

        // Add the root file to the table.
        let mut root_paths = HashSet::new();
        root_paths.insert(root.to_owned());
        table.paths.insert(INodeNo::ROOT, root_paths);

        table
    }

    /// Insert the given `path` and liteboxfs file `id` into the table and return the file's inode.
    pub fn insert(&mut self, path: PathBuf, id: FileId) -> INodeNo {
        if !self.inodes_by_file.contains_key(&id) {
            let inode = INodeNo(self.pool.next());
            self.inodes_by_file.insert(id, inode);
            self.files_by_inode.insert(inode, id);
        }

        let inode = self.inodes_by_file.get(&id).copied().unwrap();
        self.paths.entry(inode).or_default().insert(path);

        inode
    }

    /// Remove the file with the given `id` and `path` from the table.
    ///
    /// This returns `true` if the file was removed or `false` if it did not exist in the table.
    pub fn remove(&mut self, id: FileId, path: &Path) -> bool {
        let inode = match self.inodes_by_file.get(&id) {
            Some(inode) => *inode,
            None => return false,
        };

        match self.paths.entry(inode) {
            Entry::Occupied(mut entry) => {
                entry.get_mut().remove(path);

                if entry.get().is_empty() {
                    entry.remove();

                    self.files_by_inode.remove(&inode);
                    self.inodes_by_file.remove(&id);

                    self.pool.recycle(inode.0);

                    let Generation(generation) = self
                        .generations
                        .entry(inode)
                        .or_insert_with(|| Generation(0));

                    *generation += 1;
                }
            }
            Entry::Vacant(_) => unreachable!(),
        }

        true
    }

    /// Change one of the paths associated with the given `inode`.
    ///
    /// This returns `true` if the path was changed or `false` if `inode` is not in the table or
    /// `old_path` is not associated with `inode`.
    pub fn remap(&mut self, inode: INodeNo, old_path: &Path, new_path: PathBuf) -> bool {
        match self.paths.get_mut(&inode) {
            None => false,
            Some(paths) => {
                if !paths.remove(old_path) {
                    return false;
                }
                paths.insert(new_path);
                true
            }
        }
    }

    /// Get a path associated with `inode` or `None` if it is not in the table.
    pub fn path(&self, inode: INodeNo) -> Option<&Path> {
        self.paths
            .get(&inode)
            .map(|path_set| path_set.iter().next().unwrap().as_ref())
    }

    /// Get the inode associated with the given liteboxfs file `id` or `None` if it is not in the table.
    pub fn inode(&self, id: FileId) -> Option<INodeNo> {
        self.inodes_by_file.get(&id).copied()
    }

    /// Remap all cached paths that start with `old_prefix` to start with `new_prefix`.
    pub fn remap_prefix(&mut self, old_prefix: &Path, new_prefix: &Path) {
        for paths in self.paths.values_mut() {
            let to_remap: Vec<PathBuf> = paths
                .iter()
                .filter(|p| p.starts_with(old_prefix))
                .cloned()
                .collect();

            for old_path in to_remap {
                let suffix = old_path.strip_prefix(old_prefix).unwrap();
                paths.remove(&old_path);
                paths.insert(new_prefix.join(suffix));
            }
        }
    }

    /// Return the generation number associated with the given `inode`.
    pub fn generation(&self, inode: INodeNo) -> Generation {
        self.generations
            .get(&inode)
            .copied()
            .unwrap_or(Generation(0))
    }
}