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
//! Bubblewrap argument synthesiser
//!
//! This module uses a Builder following the [typestate
//! pattern](http://cliffle.com/blog/rust-typestate/) to arrange the arguments to bubblewrap in the
//! correct order. The typestate is used to ensure that the minimum requirement for a build - the
//! chroot and the command are available before the build.

use std::path::Path;

/// Builder for Args
///
/// This stores the various arguments separately until all are ready for combining.
#[derive(Default)]
pub struct ArgsBuilder<T1: Emptiable, T2: Emptiable> {
    args: CommonArgs,
    chroot: T1,
    command: T2,
}

#[derive(Default)]
struct CommonArgs {
    namespace: Vec<String>,
    user: Vec<String>,
    sysmounts: Vec<String>,
    usermounts: Vec<String>,
    tmpfs: Vec<String>,
    add_cap: Vec<String>,
    variables: Vec<String>,
    resolv: Vec<String>,
    directory: Vec<String>,
}

pub trait Emptiable {}
#[derive(Default)]
pub struct Emptied;
impl Emptiable for Emptied {}
impl Emptiable for Vec<String> {}
// Type states: (indicates presence of chroot and command)
#[allow(dead_code)]
pub type HasNeither = ArgsBuilder<Emptied, Emptied>;
#[allow(dead_code)]
type HasChroot = ArgsBuilder<Vec<String>, Emptied>;
#[allow(dead_code)]
type HasCommand = ArgsBuilder<Emptied, Vec<String>>;
#[allow(dead_code)]
type HasBoth = ArgsBuilder<Vec<String>, Vec<String>>;

impl<T1, T2> ArgsBuilder<T1, T2>
where
    T1: Emptiable,
    T2: Emptiable,
{
    pub fn new() -> HasNeither {
        HasNeither::default()
    }

    fn _mount(src: &Path, dest: &Path, ro: bool) -> Vec<String> {
        vec![
            (if ro { "--ro-bind" } else { "--bind" }).to_string(),
            format!("{}", src.display()),
            format!("{}", dest.display()),
        ]
    }

    fn _chroot(self, dir: &Path) -> ArgsBuilder<Vec<String>, T2> {
        ArgsBuilder::<Vec<String>, T2> {
            chroot: Self::_mount(dir, Path::new("/"), false),
            command: self.command,
            args: self.args,
        }
    }

    fn _command(self, cmd: &str) -> ArgsBuilder<T1, Vec<String>> {
        ArgsBuilder::<T1, Vec<String>> {
            chroot: self.chroot,
            command: cmd.split_whitespace().map(|x| x.to_string()).collect(),
            args: self.args,
        }
    }

    fn _array2vec(array: &[&str]) -> Vec<String> {
        array.iter().map(|x| x.to_string()).collect()
    }

    pub fn usercfg(mut self, args: &[&str]) -> Self {
        self.args.user = args.iter().map(|x| x.to_string()).collect();
        self
    }

    pub fn namespace(mut self, args: &[&str]) -> Self {
        self.args.namespace = args.iter().map(|x| x.to_string()).collect();
        self
    }

    pub fn sysmounts(mut self, args: &[&str]) -> Self {
        self.args.sysmounts = args.iter().map(|x| x.to_string()).collect();
        self
    }

    pub fn resolve(mut self) -> Self {
        let resolv = Path::new("/etc/resolv.conf");
        self.args.resolv = Self::_mount(resolv, resolv, true);
        self
    }

    pub fn capabilities(mut self, caps: &[&str]) -> Self {
        for cap in caps {
            self.args.add_cap.push("--cap-add".to_string());
            self.args.add_cap.push(cap.to_string());
        }
        self
    }

    pub fn add_var(mut self, var: &str, val: &str) -> Self {
        let mut vec = Self::_array2vec(&["--setenv", var, val]);
        self.args.variables.append(&mut vec);
        self
    }

    pub fn tmpfs(mut self, size: usize) -> Self {
        if size != 0 {
            const GB: usize = 2usize.pow(30);
            let size = format!("{}", size * GB);
            let args = [
                "--perms",
                "0775",
                "--size",
                &size,
                "--tmpfs",
                "/var/tmp/portage",
            ];
            self.args.tmpfs = Self::_array2vec(&args);
        }
        self
    }

    pub fn add_mount(mut self, src: &Path, dest: &Path) -> Self {
        let mut mount = Self::_mount(src, dest, true);
        self.args.usermounts.append(&mut mount);
        self
    }

    pub fn chdir(mut self, dir: &Path) -> Self {
        self.args.directory = vec!["--chdir".to_string(), dir.display().to_string()];
        self
    }
}

impl HasBoth {
    pub fn build(mut self) -> Vec<String> {
        let mut x = Vec::new();
        // Add namespace arguments
        x.append(&mut self.args.namespace);
        // Add capabilites
        x.append(&mut self.args.add_cap);
        // Add user arguments
        x.append(&mut self.args.user);
        // Add environment variables
        x.append(&mut self.args.variables);
        // Add root mount
        x.append(&mut self.chroot);
        // Add system mounts
        x.append(&mut self.args.sysmounts);
        // Add tmpfs for portage build directory if needed
        x.append(&mut self.args.tmpfs);
        // Add mount of resolv.conf
        x.append(&mut self.args.resolv);
        // Add user mounts
        x.append(&mut self.args.usermounts);
        // Change directory
        x.append(&mut self.args.directory);
        // Add command
        x.append(&mut self.command);
        x
    }
}

impl HasNeither {
    #[allow(dead_code)]
    pub fn chroot(self, dir: &Path) -> HasChroot {
        self._chroot(dir)
    }

    #[allow(dead_code)]
    pub fn command(self, cmd: &str) -> HasCommand {
        self._command(cmd)
    }
}

impl HasChroot {
    #[allow(dead_code)]
    pub fn command(self, cmd: &str) -> HasBoth {
        self._command(cmd)
    }
}

impl HasCommand {
    #[allow(dead_code)]
    pub fn chroot(self, dir: &Path) -> HasBoth {
        self._chroot(dir)
    }
}