genpac 0.1.0

Sandbox for Gentoo ebuild development using bubblewrap
// Copyright (C) 2023 Gokul Das B
// SPDX-License-Identifier: GPL-3.0-or-later
//! Chroots module
//!
//! This module is used to find the location of chroot directory and do validity checks on it.

use anyhow::Result as AResult;
use std::path::{Path, PathBuf};

struct State {
    name: String,
    path: PathBuf,
    valid: Validity,
}

pub struct Chroot<M: Verification> {
    state: State,
    marker: std::marker::PhantomData<M>,
}

// Typestates for chroot, based on validity check
pub trait Verification {}

pub struct Unverified;
impl Verification for Unverified {}
pub type ChrootUnverified = Chroot<Unverified>;

pub struct Verified;
impl Verification for Verified {}
pub type ChrootVerified = Chroot<Verified>;

#[derive(PartialEq)]
enum Validity {
    Exists,
    Absent,
    IsFile,
    AbsoluteName,
    MultipleLevels,
    InvalidName,
}

impl<M: Verification> Chroot<M> {
    pub(super) fn new(name: &Path, workspace: &Path) -> ChrootUnverified {
        use Validity::*;
        let path = workspace.join(name);

        let valid = if name.is_absolute() {
            AbsoluteName
        } else if name.parent() != Some(Path::new("")) {
            MultipleLevels
        } else if name.file_name().is_none() {
            InvalidName
        } else if path.is_dir() {
            Exists
        } else if path.is_file() {
            IsFile
        } else {
            Absent
        };

        let name = name.to_string_lossy().into_owned();
        let state = State { name, path, valid };
        let marker = std::marker::PhantomData;
        ChrootUnverified { state, marker }
    }
}

impl ChrootUnverified {
    /// Verify validity of chroot dir based on if it should exist or not
    pub fn verify(self, must_exist: bool) -> AResult<ChrootVerified> {
        use Validity::*;
        let state = self.state;

        // This when we want the chroot to already exist
        let exist1 = must_exist && state.valid == Exists;

        // This is when the chroot must not exist. eg: before creating one
        let exist2 = !must_exist && state.valid == Absent;

        if exist1 || exist2 {
            let marker = std::marker::PhantomData;
            Ok(Chroot { state, marker })
        // Everything else is an error
        } else {
            let name = state.name.as_str();
            let msg = match state.valid {
                Exists => format!("Chroot '{name}' already exists in workspace"),
                Absent => format!("Chroot '{name}' doesn't exist in workspace"),
                IsFile => format!("'{name}' is a file in the workspace"),
                AbsoluteName => "Absolute paths are not valid chroot names".to_string(),
                MultipleLevels => "Chroot name shouldn't have multiple levels".to_string(),
                InvalidName => "Chroot name is invalid".to_string(),
            };
            anyhow::bail!(msg);
        }
    }
}

// Allow direct use of Chroot in messages
impl<M: Verification> std::fmt::Display for Chroot<M> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.state.name)
    }
}

// Allow direct use of Chroot as Path (only if it has been verified)
impl std::ops::Deref for ChrootVerified {
    type Target = Path;

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.state.path.as_path()
    }
}