liteboxfs 0.1.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::{
    borrow::Cow,
    fmt::{self, Debug, Display},
    str::FromStr,
    time::SystemTime,
};

use rusqlite::ToSql;
use uuid::Uuid;

use crate::{
    block::FileId as StoreFileId, errors::InternalError, hash::MerkleHash, sql::SqlStoreGuard,
};

/// The name of the default filesystem root.
///
/// This is the string `default`.
pub const DEFAULT_ROOT_NAME: &str = "default";

/// A [`FilesystemId`] is a UUID that uniquely identifies a filesystem.
///
/// A SQLite database contains a single LiteboxFS filesystem, meaning the terms "filesystem" and
/// "database" are synonymous. However, a filesystem can contain multiple roots, each identified by
/// a [`RootId`]. Don't confuse these concepts!
///
/// This value can be serialized and deserialized using [`Display`] and [`FromStr`].
///
/// You can get the [`FilesystemId`] of a filesystem with [`Filesystem::filesystem_id`].
///
/// [`Filesystem::filesystem_id`]: crate::Filesystem::filesystem_id
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FilesystemId {
    filesystem_id: Uuid,
}

impl FilesystemId {
    pub(crate) fn new() -> Self {
        Self {
            filesystem_id: Uuid::new_v4(),
        }
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for FilesystemId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("FilesystemId")
            .field(&self.filesystem_id.to_string())
            .finish()
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Display for FilesystemId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.filesystem_id)
    }
}

impl FromStr for FilesystemId {
    type Err = crate::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let filesystem_id = Uuid::parse_str(s).map_err(|_| InternalError::InvalidUuid {
            uuid: s.to_string(),
        })?;

        Ok(Self { filesystem_id })
    }
}

/// A [`RootId`] is a UUID that uniquely identifies a filesystem root.
///
/// Unlike a traditional POSIX filesystem, LiteboxFS supports multiple root directories, each
/// identified by a [`RootId`]. This allows for multiple independent logical filesystems to coexist
/// within the same database.
///
/// This value can be serialized and deserialized using [`Display`] and [`FromStr`].
///
/// You can get the current [`RootId`] of a filesystem with [`Filesystem::root_id`].
///
/// [`Filesystem::root_id`]: crate::Filesystem::root_id
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct RootId {
    root_id: Uuid,
}

impl RootId {
    pub(crate) fn new() -> Self {
        Self {
            root_id: Uuid::new_v4(),
        }
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for RootId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("RootId")
            .field(&self.root_id.to_string())
            .finish()
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Display for RootId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.root_id)
    }
}

impl FromStr for RootId {
    type Err = crate::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let root_id = Uuid::parse_str(s).map_err(|_| InternalError::InvalidUuid {
            uuid: s.to_string(),
        })?;

        Ok(Self { root_id })
    }
}

#[doc(hidden)]
impl ToSql for RootId {
    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
        Ok(rusqlite::types::ToSqlOutput::from(self.root_id.to_string()))
    }
}

/// The ID and optional name of a filesystem root.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Root {
    /// The universally unique ID of the root.
    pub id: RootId,

    /// The optional name of the root.
    pub name: Option<String>,

    /// The time the root was created.
    pub created: SystemTime,
}

/// A selector for finding a filesystem root.
///
/// `RootBy::Default` always evaluates as equal to `RootBy::Name(DEFAULT_ROOT_NAME)`:
///
/// ```
/// # use liteboxfs::{RootBy, DEFAULT_ROOT_NAME};
/// assert_eq!(RootBy::Default, DEFAULT_ROOT_NAME.into());
/// ```
#[derive(Debug, Clone)]
pub enum RootBy<'a> {
    /// Get the default root.
    Default,

    /// Get the root with the given [`RootId`].
    Id(RootId),

    /// Get the root with the given name.
    ///
    /// Root names are unique, but roots are not required to have a name.
    Name(Cow<'a, str>),
}

impl PartialEq for RootBy<'_> {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (RootBy::Default, RootBy::Default) => true,
            (RootBy::Id(a), RootBy::Id(b)) => a == b,
            (RootBy::Name(a), RootBy::Name(b)) => a == b,
            (RootBy::Default, RootBy::Name(name)) | (RootBy::Name(name), RootBy::Default) => {
                name == DEFAULT_ROOT_NAME
            }
            _ => false,
        }
    }
}

impl Eq for RootBy<'_> {}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Display for RootBy<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RootBy::Default => write!(f, "{}", DEFAULT_ROOT_NAME),
            RootBy::Id(root_id) => write!(f, "{}", root_id),
            RootBy::Name(name) => write!(f, "{}", name),
        }
    }
}

impl From<RootId> for RootBy<'_> {
    fn from(root_id: RootId) -> Self {
        Self::Id(root_id)
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl From<String> for RootBy<'_> {
    fn from(name: String) -> Self {
        RootBy::from(Cow::Owned(name))
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a str> for RootBy<'a> {
    fn from(name: &'a str) -> Self {
        RootBy::from(Cow::Borrowed(name))
    }
}

impl<'a> From<Cow<'a, str>> for RootBy<'a> {
    fn from(name: Cow<'a, str>) -> Self {
        if name == DEFAULT_ROOT_NAME {
            Self::Default
        } else {
            Self::Name(name)
        }
    }
}

const ROOT_PAGE_SIZE: u32 = 100;

/// An iterator over a filesystem's [`Root`]s.
///
/// You can get a [`Roots`] iterator with [`Filesystem::list_roots`].
///
/// [`Filesystem::list_roots`]: crate::Filesystem::list_roots
#[derive(Debug)]
pub struct Roots<'conn, 'fs> {
    store: &'fs mut SqlStoreGuard<'conn>,
    page: Vec<Root>,
    offset: u32,
    limit: u32,
}

impl<'conn, 'fs> Roots<'conn, 'fs> {
    pub(crate) fn new(store: &'fs mut SqlStoreGuard<'conn>) -> Self {
        Self {
            store,
            page: Vec::new(),
            offset: 0,
            limit: ROOT_PAGE_SIZE,
        }
    }
}

impl<'conn, 'fs> Iterator for Roots<'conn, 'fs> {
    type Item = crate::Result<Root>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.page.pop() {
            Some(root) => Some(Ok(root)),
            None => {
                match self
                    .store
                    .exec(|store| store.get_roots_page(self.limit, self.offset))
                {
                    Ok(new_page) => {
                        if new_page.is_empty() {
                            return None;
                        }

                        self.page = new_page;
                        self.offset += self.page.len() as u32;

                        self.page.pop().map(Ok)
                    }
                    Err(err) => Some(Err(err)),
                }
            }
        }
    }
}

/// A [`ContentId`] uniquely identifies the content of a file in a filesystem.
///
/// A [`ContentId`] is like an opaque hash of the file's content, except it's cheaper to compute.
/// This value can be used to quickly determine if two files have the same content. This works
/// across different roots, but not across different filesystems; two [`ContentId`]s from different
/// filesystems are never equal, even if the underlying content is the same.
///
/// You can get a [`ContentId`] with [`File::content_id`].
///
/// [`File::content_id`]: crate::File::content_id
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ContentId {
    merkle_hash: MerkleHash,

    // Content IDs are only comparable within the same filesystem because the parameters used to
    // compute the merkle tree can differ. To enforce that Content IDs from different filesystems
    // are never equal, we include the filesystem ID here.
    filesystem_id: FilesystemId,
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for ContentId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("ContentId")
            .field(&self.merkle_hash.to_string())
            .finish()
    }
}

impl ContentId {
    pub(crate) fn new(merkle_hash: MerkleHash, filesystem_id: FilesystemId) -> Self {
        Self {
            merkle_hash,
            filesystem_id,
        }
    }
}

/// A [`FileId`] uniquely identifies a file in a filesystem.
///
/// This is analogous to an inode number in a traditional POSIX filesystem. It can be used to:
///
/// - Open a file (via [`Filesystem::open`]).
/// - Determine if two [`File`]s are hard links to the same underlying file.
/// - Detect filesystem loops.
///
/// You can get a [`FileId`] with [`File::file_id`].
///
/// [`Filesystem::open`]: crate::Filesystem::open
/// [`File`]: crate::File
/// [`File::file_id`]: crate::File::file_id
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileId {
    file_id: crate::block::FileId,

    // The database ID for files are not not universally unique, so we need to include the
    // filesystem ID here to ensure file IDs from different filesystems are never equal.
    filesystem_id: FilesystemId,
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for FileId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", i64::from(self.file_id))
    }
}

impl FileId {
    pub(crate) fn new(file_id: StoreFileId, filesystem_id: FilesystemId) -> Self {
        Self {
            file_id,
            filesystem_id,
        }
    }
}

#[doc(hidden)]
impl ToSql for FileId {
    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
        self.file_id.to_sql()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use xpct::{be_err, be_ok, equal, expect};

    #[test]
    fn default_string_becomes_default_enum_variant() {
        let default_root: RootBy = DEFAULT_ROOT_NAME.into();
        expect!(default_root).to(equal(RootBy::Default));

        let not_default_root: RootBy = "not-default".into();
        expect!(not_default_root).to(equal(RootBy::Name("not-default".into())));
    }

    #[test]
    fn root_by_equality() {
        let root_id = RootId::new();

        expect!(RootBy::Default).to(equal(RootBy::Default));
        expect!(RootBy::Name("foo".into())).to(equal(RootBy::Name("foo".into())));
        expect!(RootBy::Id(root_id)).to(equal(RootBy::Id(root_id)));
        expect!(RootBy::Default).to(equal(RootBy::Name(DEFAULT_ROOT_NAME.into())));

        expect!(RootBy::Default).to_not(equal(RootBy::Id(root_id)));
        expect!(RootBy::Default).to_not(equal(RootBy::Name("not-default".into())));
        expect!(RootBy::Name("foo".into())).to_not(equal(RootBy::Name("bar".into())));
        expect!(RootBy::Id(root_id)).to_not(equal(RootBy::Id(RootId::new())));
    }

    #[test]
    fn parse_filesystem_id() {
        let uuid = "1ca7ac38-24a4-46e8-ae39-b45290be662c";

        expect!(FilesystemId::from_str(uuid))
            .to(be_ok())
            .map(|id| id.to_string())
            .to(equal(uuid));

        let invalid_filesystem_id_str = "not-a-uuid";
        expect!(FilesystemId::from_str(invalid_filesystem_id_str)).to(be_err());
    }
}