tartarus-api 0.1.1

Structured API for sandboxing system (currently utilizing `bubblewrap`)
Documentation
#[cfg(feature = "config_file")]
pub mod file;
pub mod overrides;

use crate::{
    error::Result,
    sandbox::{Sandbox, SandboxType},
};
use std::{
    fmt::Debug,
    fs::File,
    io::{BufReader, BufWriter, Read, Write},
    path::{Path, PathBuf},
};

/// The main configuration struct for the sandbox.
///
/// If at least one override or passthrough home directory is specified, the sandbox will use a
/// temporary directory for the home directory instead of the user's actual one. The original home
/// directory can still be accessed via absolute paths (and will be read-only by default like any
/// other directory not explicitly specified as writable).
#[derive(Debug)]
pub struct SandboxConfig {
    /// Whether network access is allowed for the sandbox.
    pub allow_network_access: bool,

    /// The directories to mounted as writable for the sandbox.
    pub writable_dirs: Option<Vec<PathBuf>>,

    /// Directories to recursively copy into the sandbox under a temporary directory rather than
    /// mapping the real ones.
    ///
    /// Overriden home directories are mounted as writable for the sandbox.
    pub override_home_dirs: Option<Vec<OverrideHomeDir>>,

    /// Directories to directly mount from the user's home directory under the override home
    /// directory.
    ///
    /// Passthrough directories are mounted as writable for the sandbox.
    pub passthrough_home_dirs: Option<Vec<PathBuf>>,
}

impl Default for SandboxConfig {
    fn default() -> Self {
        Self::new()
    }
}

impl SandboxConfig {
    /// Creates a new configuration with no network access, no writable directories, and no fake
    /// home directory
    pub const fn new() -> Self {
        Self {
            allow_network_access: false,
            writable_dirs: None,
            override_home_dirs: None,
            passthrough_home_dirs: None,
        }
    }

    /// Enables network access for the sandbox.
    pub const fn with_network_access(&mut self) -> &mut Self {
        self.allow_network_access = true;
        self
    }

    /// Marks a directory as writable for the sandbox.
    pub fn with_writable_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
        self.writable_dirs.get_or_insert_default().push(dir.into());
        self
    }

    /// Marks a subdirectory of the user's home as an override.
    pub fn with_override_home_dir(&mut self, override_home_dir: OverrideHomeDir) -> &mut Self {
        self.override_home_dirs
            .get_or_insert_default()
            .push(override_home_dir);
        self
    }

    /// Marks a subdirectory of the user's home as passthrough.
    pub fn with_passthrough_home_dir(
        &mut self,
        segments: impl IntoIterator<Item = impl AsRef<Path>>,
    ) -> &mut Self {
        self.passthrough_home_dirs
            .get_or_insert_default()
            .push(PathBuf::from_iter(segments));
        self
    }

    /// Builds a sandbox with the given type.
    pub const fn build_sandbox(&mut self, sandbox_type: SandboxType) -> Sandbox {
        Sandbox {
            sandbox_type,
            config: Self {
                allow_network_access: self.allow_network_access,
                writable_dirs: self.writable_dirs.take(),
                override_home_dirs: self.override_home_dirs.take(),
                passthrough_home_dirs: self.passthrough_home_dirs.take(),
            },
        }
    }

    /// Returns whether the sandbox should configure a fake home directory.
    pub(crate) fn needs_fake_home(&self) -> bool {
        self.override_home_dirs
            .as_ref()
            .is_some_and(|v| !v.is_empty()) ||
            self.passthrough_home_dirs
                .as_ref()
                .is_some_and(|v| !v.is_empty())
    }
}

/// A directory under the home directory to recursively copy into the sandbox under a temporary
/// directory rather than mapping the real one.
#[derive(Debug)]
pub struct OverrideHomeDir {
    /// The path of the directory to copy from the host filesystem. These paths are interpreted as
    /// relative paths below the user's home directory.
    pub subpath: PathBuf,

    /// Arbitrary extra settings to apply to files mapped to the sandbox
    pub overrides: Option<Vec<OverrideFile<Box<dyn Override>>>>,
}

impl OverrideHomeDir {
    /// Creates a new override home directory for the given path.
    pub fn new(path_segments: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
        Self {
            subpath: PathBuf::from_iter(path_segments),
            overrides: None,
        }
    }

    /// Adds an override for the given path with the given behavior.
    pub fn with_override(
        &mut self,
        path: impl Into<PathBuf>,
        behavior: impl Override + 'static,
    ) -> &mut Self {
        self.overrides.get_or_insert_default().push(OverrideFile {
            path: path.into(),
            behavior: Box::new(behavior),
        });
        self
    }

    /// Helper function to get an owned [`OverrideHomeDir`] instance from a mutable reference to
    /// facilitate builder-style chaining.
    #[must_use]
    pub fn take(&mut self) -> Self {
        Self {
            subpath: std::mem::take(&mut self.subpath),
            overrides: self.overrides.take(),
        }
    }
}

/// Specifies how to override a file from the user's home directory in the sandbox.
#[derive(Debug)]
pub struct OverrideFile<T> {
    pub path: PathBuf,
    pub behavior: T,
}

/// Provides lazy access to the file being overridden in the sandbox and a sink for the contents of
/// the file to override with in the sandbox.
#[derive(Debug)]
pub struct FileContents {
    pub(crate) original: BufReader<File>,
    pub(crate) output: BufWriter<File>,
}

impl FileContents {
    /// Returns a [`Read`] instance for reading the original file contents.
    pub fn read(&mut self) -> impl Read {
        &mut self.original
    }

    /// Returns a [`Write`] instance for writing the sandboxed file contents.
    pub fn write(&mut self) -> impl Write {
        &mut self.output
    }
}

/// A trait providing a way to define how a file should be overridden in the sandbox.
///
/// This can be used to inject extra configurations to the sandbox that you don't want to live in
/// your standard configurations (bypassing permission checks, allowing arbitrary tool usage, etc.)
pub trait Override: Debug {
    /// Defines how the original file should be processed and mapped to the sandbox.
    ///
    /// # Arguments
    ///
    /// - `contents`: Provides access to the original file contents for reading and the sandboxed
    ///   output file for writing.
    ///
    /// # Errors
    ///
    /// Returns an error if the setting could not be applied.
    fn apply(&self, contents: FileContents) -> Result<()>;
}