timefs 0.1.0

Mount a Git repository as a read-only filesystem.
Documentation
//! Validated runtime configuration derived from CLI input.

use crate::cli::{Cli, Command, MountArgs, SubmodulesMode, UnmountArgs};
use crate::errors::{Error, Result};
use fuser::MountOption;
use std::path::{Path, PathBuf};

/// The validated command selected by the user.
#[derive(Debug, Clone)]
pub enum AppCommand {
    /// Mount a repository at the requested mountpoint.
    Mount(MountConfig),
    /// Unmount an existing mountpoint.
    Unmount(UnmountConfig),
}

impl AppCommand {
    /// Convert parsed CLI arguments into validated runtime configuration.
    pub fn from_cli(cli: Cli) -> Result<Self> {
        match cli.command {
            Command::Mount(args) => MountConfig::try_from(args).map(Self::Mount),
            Command::Unmount(args) => UnmountConfig::try_from(args).map(Self::Unmount),
        }
    }
}

/// Validated configuration for a `timefs mount` request.
#[derive(Debug, Clone)]
pub struct MountConfig {
    /// Canonical path to the Git repository.
    pub repo: PathBuf,
    /// Canonical path to the mountpoint directory.
    pub mountpoint: PathBuf,
    /// Run in the foreground when `true`.
    pub foreground: bool,
    /// Allow access for other users.
    pub allow_other: bool,
    /// Strategy for submodule exposure.
    pub submodules: SubmodulesMode,
    /// Resolve local Git LFS objects when available.
    pub lfs: bool,
    /// Freeze ref resolution at mount time.
    pub ref_snapshot: bool,
    /// In-memory cache size in MiB.
    pub cache_size_mb: usize,
    /// Reported UID override.
    pub uid: Option<u32>,
    /// Reported GID override.
    pub gid: Option<u32>,
}

impl MountConfig {
    /// The FUSE mount options required by the project invariants.
    pub fn fuse_mount_options(&self) -> Vec<MountOption> {
        let mut options = vec![
            MountOption::RO,
            MountOption::DefaultPermissions,
            MountOption::FSName(String::from("timefs")),
            MountOption::Subtype(String::from("timefs")),
        ];

        if self.allow_other {
            options.push(MountOption::AllowOther);
        }

        options
    }
}

impl TryFrom<MountArgs> for MountConfig {
    type Error = Error;

    fn try_from(args: MountArgs) -> Result<Self> {
        let repo = validate_existing_dir(&args.repo, "repository")?;
        let mountpoint = validate_existing_dir(&args.mountpoint, "mountpoint")?;

        Ok(Self {
            repo,
            mountpoint,
            foreground: args.foreground,
            allow_other: args.allow_other,
            submodules: args.submodules,
            lfs: args.lfs,
            ref_snapshot: args.ref_snapshot,
            cache_size_mb: args.cache_size_mb,
            uid: args.uid,
            gid: args.gid,
        })
    }
}

/// Validated configuration for a `timefs unmount` request.
#[derive(Debug, Clone)]
pub struct UnmountConfig {
    /// Canonical path to the mountpoint directory.
    pub mountpoint: PathBuf,
}

impl TryFrom<UnmountArgs> for UnmountConfig {
    type Error = Error;

    fn try_from(args: UnmountArgs) -> Result<Self> {
        let mountpoint = validate_existing_dir(&args.mountpoint, "mountpoint")?;
        Ok(Self { mountpoint })
    }
}

fn validate_existing_dir(path: &Path, label: &'static str) -> Result<PathBuf> {
    match path.try_exists() {
        Ok(true) => {}
        Ok(false) => {
            return Err(Error::PathDoesNotExist {
                label,
                path: path.to_path_buf(),
            });
        }
        Err(source) => {
            return Err(Error::PathProbeFailed {
                label,
                path: path.to_path_buf(),
                source,
            });
        }
    }

    let metadata = std::fs::metadata(path).map_err(|source| Error::PathProbeFailed {
        label,
        path: path.to_path_buf(),
        source,
    })?;

    if !metadata.is_dir() {
        return Err(Error::PathIsNotDirectory {
            label,
            path: path.to_path_buf(),
        });
    }

    std::fs::canonicalize(path).map_err(|source| Error::PathCanonicalizeFailed {
        label,
        path: path.to_path_buf(),
        source,
    })
}