liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::{
    convert::Infallible,
    fmt::{self, Debug, Display},
    io,
    path::PathBuf,
    result,
    str::FromStr,
};

use thiserror::Error as DeriveError;

use crate::{
    FileOrigin, RootBy,
    block::{BlockId, FileId},
};

#[derive(Debug, Clone, PartialEq, Eq, DeriveError)]
pub enum InternalError {
    #[error("File with ID {id:?} not found.")]
    FileNotFound { id: FileId },
    #[error("Block with ID {id:?} not found.")]
    BlockNotFound { id: BlockId },
    #[error("Invalid UUID: {uuid}.")]
    InvalidUuid { uuid: String },
    #[error("Root ID in the database is not a valid UUID: {uuid}")]
    RootIdIsNotAUuid { uuid: String },
    #[error("Could not read filesystem settings.")]
    MalformedSettings,
    #[error("Block hash was not exactly 32 bytes long.")]
    IncorrectBlockHashLen,
    #[error("Block has a hash but not data or data but not a hash.")]
    MismatchedBlockHashAndData,
    #[error("Invalid file kind: {kind}")]
    InvalidFileKind { kind: String },
    #[error("Cannot insert root path into closure table.")]
    RootPathCannotBeInserted,
    #[error("Feature 'compression' is disabled.")]
    CompressionDisabled,
    #[error("Feature 'chunking' is disabled.")]
    ChunkingDisabled,
    #[error("Cannot move or copy a directory into one of its descendants.")]
    CannotMoveIntoDescendant,
    #[error("Cannot delete or unlink the root directory.")]
    CannotDeleteRootDir,
    #[error("ACL is malformed.")]
    MalformedAcl,
    #[error("Timestamp is malformed.")]
    MalformedTimeBlob,
    #[error("There was unexpectedly more than one root merkle node.")]
    MoreThanOneRootMerkleNode,
    #[error("Error serializing or deserializing metadata: {reason}")]
    MetadataSerializationError { reason: String },
    #[error("Mutually exclusive walk options were specified.")]
    MutuallyExclusiveWalkOptions,
    #[error("{reason}")]
    Other { reason: String },
}

/// The opaque underlying reason for an error.
///
/// This serves as a hint about what caused an error to occur, but must not be parsed or
/// interpreted programmatically.
#[derive(Clone, PartialEq, Eq)]
pub struct ErrorReason(pub(crate) InternalError);

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

#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for ErrorReason {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self.0.to_string())
    }
}

#[doc(hidden)]
impl FromStr for ErrorReason {
    type Err = Infallible;

    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
        Ok(Self(InternalError::Other {
            reason: s.to_string(),
        }))
    }
}

impl From<InternalError> for ErrorReason {
    fn from(err: InternalError) -> Self {
        Self(err)
    }
}

impl From<InternalError> for Error {
    fn from(err: InternalError) -> Self {
        match err {
            InternalError::MalformedSettings => crate::Error::Malformed { reason: err.into() },
            InternalError::MalformedTimeBlob => crate::Error::Malformed { reason: err.into() },
            InternalError::IncorrectBlockHashLen => crate::Error::Malformed { reason: err.into() },
            InternalError::RootIdIsNotAUuid { .. } => {
                crate::Error::Malformed { reason: err.into() }
            }
            InternalError::CompressionDisabled | InternalError::ChunkingDisabled => {
                crate::Error::FeatureDisabled { reason: err.into() }
            }
            InternalError::MutuallyExclusiveWalkOptions => {
                crate::Error::InvalidArgs { reason: err.into() }
            }
            _ => crate::Error::Other { reason: err.into() },
        }
    }
}

/// An opaque type representing a SQLite error code.
#[derive(Clone, PartialEq, Eq)]
pub struct SqliteErrorCode {
    code: Option<rusqlite::ffi::Error>,
    reason: String,
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Debug for SqliteErrorCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut debug = &mut f.debug_struct("SqliteErrorCode");

        if let Some(code) = &self.code {
            debug = debug.field("code", &format!("{}", code.extended_code));
        }

        debug.field("reason", &self.reason).finish()
    }
}

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

impl SqliteErrorCode {
    /// The raw extended error code from the SQLite C API, if there is one.
    ///
    /// See the [SQLite docs](https://www.sqlite.org/rescode.html) for more information.
    pub fn raw_code(&self) -> Option<std::ffi::c_int> {
        // We're not including rusqlite in our public API, so we're only exposing the raw error
        // code from the SQLite C API as opposed to any rusqlite types.
        self.code.map(|err| err.extended_code)
    }
}

/// The error type for liteboxfs.
#[derive(Debug, Clone, PartialEq, Eq, DeriveError)]
#[non_exhaustive]
pub enum Error {
    /// Some arguments were invalid.
    #[error("Some arguments were invalid: {reason}")]
    InvalidArgs {
        /// A hint about why this error occurred.
        reason: ErrorReason,
    },

    /// Could not find a file in a filesystem.
    #[error("The specified file was not found: {file:?}")]
    FileNotFound {
        /// The location of the file in question.
        file: FileOrigin<'static>,
    },

    /// A file already exists at the specified path.
    #[error("This file already exists: {path:?}")]
    FileAlreadyExists {
        /// The path in question.
        path: FileOrigin<'static, PathBuf>,
    },

    /// The specified file has no parent directory.
    #[error("This file has no parent directory: {file:?}")]
    NoParentDirectory {
        /// The file in question.
        file: FileOrigin<'static>,
    },

    /// The specified file is not a directory.
    #[error("This file is not a directory: {file:?}")]
    NotADirectory {
        /// The file in question.
        file: FileOrigin<'static>,
    },

    /// The specified directory is not empty.
    #[error("This directory is not empty: {path:?}")]
    DirectoryNotEmpty {
        /// The path of the directory in question.
        path: FileOrigin<'static, PathBuf>,
    },

    /// The specified path is not valid.
    #[error("This path is not valid: {path:?}\n{reason}")]
    InvalidPath {
        /// The path in question.
        path: FileOrigin<'static, PathBuf>,

        /// A hint about why this error occurred.
        reason: ErrorReason,
    },

    /// Could not find a filesystem root with the given name or ID.
    #[error("There is no root with this name: {root}")]
    RootNotFound {
        /// The name or ID of the root that was not found.
        root: RootBy<'static>,
    },

    /// A filesystem root with the given name already exists.
    #[error("There is already a root with this name: {name}")]
    RootAlreadyExists {
        /// The name of the root that already exists.
        name: String,
    },

    /// Attempted to delete or rename the default root.
    #[error("Cannot delete or rename the default root.")]
    IsDefaultRoot,

    /// Attempted to delete the current root.
    #[error("Cannot delete the current root.")]
    IsCurrentRoot,

    /// The given file is not a regular file.
    #[error("This is not a regular file: {file:?}")]
    NotARegularFile {
        /// The file in question.
        file: FileOrigin<'static>,
    },

    /// This file is a type that's not supported by LiteboxFS.
    #[error("This file is a type that's not supported by LiteboxFS.: {file:?}")]
    UnsupportedFileType {
        /// The file in question.
        file: FileOrigin<'static>,
    },

    /// Following a symbolic link would form a loop.
    #[error("Following this symbolic link would form a loop: {path:?}")]
    SymlinkLoop {
        /// The symbolic link whose target is one of its own ancestors.
        path: FileOrigin<'static, PathBuf>,
    },

    /// Could not open the litebox.
    #[error("The litebox could not be opened.")]
    CannotOpen,

    /// Attempted to write to a read-only database.
    #[error("Attempted to write to a read-only database.")]
    ReadOnly,

    /// A filesystem limit was exceeded.
    #[error("A filesystem limit was exceeded.")]
    TooLarge,

    /// The maximum length of a [`UserMetadata`] key or value was exceeded.
    ///
    /// [`UserMetadata`]: crate::user::UserMetadata
    #[error("The maximum length of a metadata key or value was exceeded.")]
    MetadataLimitExceeded,

    /// Attempted to open a file that is not a SQLite database.
    #[error("This file is not a SQLite database.")]
    NotADatabase,

    /// Attempted to open a SQLite database that is not a litebox.
    #[error("This SQLite database is not litebox.")]
    NotALitebox,

    /// Attempted to create a new litebox, but one already exists.
    #[error("Attempted to create a new litebox, but one already exists.")]
    LiteboxAlreadyExists,

    /// Attempted to open a litebox that is exclusively locked.
    ///
    /// This happens when:
    ///
    /// - A process attempts to open a liteboxfs connection while it is mounted via FUSE by another
    ///   process.
    /// - A connection attempts to mount a litebox that has other liteboxfs connections open.
    #[error("This litebox is in use; mounting via FUSE requires an exclsuive lock on the litebox.")]
    LiteboxLocked,

    /// The litebox is a newer version than this version of the library supports.
    #[error("The format version of the litebox is not supported by this version of the library.")]
    UnsupportedFormatVersion,

    /// A feature required to open this litebox is disabled.
    #[error(
        "Cannot create or open the litebox because of a missing compile-time feature: {reason}"
    )]
    FeatureDisabled {
        /// A hint about why this error occurred.
        reason: ErrorReason,
    },

    /// This litebox is malformed or corrupted.
    #[error("This litebox is malformed or corrupted: {reason}")]
    Malformed {
        /// A hint about why this error occurred.
        reason: ErrorReason,
    },

    /// There was an error from the underlying SQLite database.
    #[error("There was an error from the underlying SQLite database: {code}")]
    Sqlite {
        /// The underlying SQLite error code, if there is one.
        code: SqliteErrorCode,
    },

    /// An I/O error occurred.
    #[error("An I/O error occurred: {kind}")]
    Io {
        /// The [`std::io::ErrorKind`] of the I/O error.
        kind: io::ErrorKind,

        /// The raw OS error code, if there is one.
        code: Option<i32>,
    },

    /// An unspecified error occurred.
    #[error("{reason}")]
    Other {
        /// A hint about why this error occurred.
        reason: ErrorReason,
    },
}

impl From<rusqlite::Error> for Error {
    fn from(err: rusqlite::Error) -> Self {
        match err.sqlite_error() {
            Some(rusqlite::ffi::Error {
                code: rusqlite::ErrorCode::ReadOnly,
                ..
            }) => Error::ReadOnly,
            Some(rusqlite::ffi::Error {
                code: rusqlite::ErrorCode::CannotOpen,
                ..
            }) => Error::CannotOpen,
            Some(rusqlite::ffi::Error {
                code: rusqlite::ErrorCode::NotADatabase,
                ..
            }) => Error::NotADatabase,
            code => Error::Sqlite {
                code: SqliteErrorCode {
                    code: code.cloned(),
                    reason: err.to_string(),
                },
            },
        }
    }
}

impl From<io::Error> for Error {
    fn from(error: io::Error) -> Self {
        let kind = error.kind();
        let code = error.raw_os_error();

        match error.into_inner() {
            Some(payload) => match payload.downcast::<Error>() {
                Ok(crate_error) => *crate_error,
                Err(_) => Error::Io { kind, code },
            },
            None => Error::Io { kind, code },
        }
    }
}

impl From<Error> for io::Error {
    fn from(err: Error) -> Self {
        let kind = match err {
            Error::Io { kind, .. } => kind,
            _ => io::ErrorKind::Other,
        };

        io::Error::new(kind, err)
    }
}

/// The result type for operations with a litebox.
pub type Result<T> = result::Result<T, Error>;