audio-plugin-bsd 0.1.1

Dynamic .so audio-plugin loader with ABI verification and FreeBSD Capsicum/pdfork per-process sandboxing for real-time audio in Rust
Documentation
//! Sandbox configuration: [`IsolationLevel`], [`SandboxConfig`], and platform
//! availability checks.
//!
//! On FreeBSD the loader can confine each plugin to a Capsicum capability
//! sandbox (`cap_enter`) or a full `pdfork` child process with per-process
//! capability restrictions. On non-FreeBSD platforms the sandbox is a no-op:
//! [`IsolationLevel::None`] is the only available option.
//!
//! # Usage
//!
//! ```rust
//! use audio_plugin_bsd::sandbox::{IsolationLevel, SandboxConfig};
//!
//! let cfg = SandboxConfig::new()
//!     .with_isolation_level(IsolationLevel::None)
//!     .with_log_channel_capacity(64);
//!
//! assert!(cfg.isolation_level().is_available());
//! ```

/// The degree of sandbox isolation applied to a plugin process.
///
/// On non-FreeBSD platforms only [`None`](IsolationLevel::None) is available;
/// requesting a stricter level yields [`PluginError::Sandbox`] at load time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IsolationLevel {
    /// No sandboxing — plugin code runs in the host process (unsafe with
    /// untrusted plugins).
    None,
    /// FreeBSD Capsicum capability mode (`cap_enter`). The plugin retains
    /// file descriptors inherited at load time but cannot open new ones.
    #[cfg(target_os = "freebsd")]
    Capsicum,
    /// Full per-plugin process isolation via `pdfork` + Capsicum. Each plugin
    /// runs in its own child process with a dedicated log channel.
    #[cfg(target_os = "freebsd")]
    CapsicumProcess,
}

impl IsolationLevel {
    /// Return `true` when this isolation level is supported on the current
    /// platform.
    #[must_use]
    pub fn is_available(self) -> bool {
        match self {
            Self::None => true,
            #[cfg(target_os = "freebsd")]
            Self::Capsicum => true,
            #[cfg(target_os = "freebsd")]
            Self::CapsicumProcess => true,
        }
    }

    /// Human-readable name for logging / diagnostics.
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::None => "none",
            #[cfg(target_os = "freebsd")]
            Self::Capsicum => "capsicum",
            #[cfg(target_os = "freebsd")]
            Self::CapsicumProcess => "capsicum-process",
        }
    }
}

/// Builder for sandbox configuration.
///
/// All fields are `Copy`, so `SandboxConfig` is cheap to clone and pass by
/// value. The default configuration uses [`IsolationLevel::None`] with a
/// 16-element log channel.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SandboxConfig {
    /// The isolation level to apply.
    isolation_level: IsolationLevel,
    /// Maximum number of log messages buffered between plugin and host when
    /// running in a `pdfork` child process. A value of 0 disables buffering
    /// (log messages are dropped immediately). Ignored when isolation is
    /// [`None`](IsolationLevel::None).
    log_channel_capacity: usize,
}

impl SandboxConfig {
    /// Construct a default configuration: no isolation, 16-element log
    /// channel.
    #[must_use]
    pub fn new() -> Self {
        Self {
            isolation_level: IsolationLevel::None,
            log_channel_capacity: 16,
        }
    }

    /// Set the isolation level.
    #[must_use]
    pub fn with_isolation_level(mut self, level: IsolationLevel) -> Self {
        self.isolation_level = level;
        self
    }

    /// Set the log channel capacity (number of buffered messages).
    ///
    /// A capacity of 0 means log messages are dropped. Values above
    /// `u32::MAX` are clamped when encoding on the wire.
    #[must_use]
    pub fn with_log_channel_capacity(mut self, capacity: usize) -> Self {
        self.log_channel_capacity = capacity;
        self
    }

    /// The configured isolation level.
    #[must_use]
    pub fn isolation_level(self) -> IsolationLevel {
        self.isolation_level
    }

    /// The configured log channel capacity.
    #[must_use]
    pub fn log_channel_capacity(self) -> usize {
        self.log_channel_capacity
    }

    /// Return `true` when the configured isolation level is supported on the
    /// current platform.
    #[must_use]
    pub fn is_platform_supported(self) -> bool {
        self.isolation_level.is_available()
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    // --- IsolationLevel ---------------------------------------------------

    #[test]
    fn none_is_always_available() {
        assert!(IsolationLevel::None.is_available());
    }

    #[test]
    fn none_as_str() {
        assert_eq!(IsolationLevel::None.as_str(), "none");
    }

    #[test]
    fn none_is_copy_clone_eq_debug_hash() {
        let a = IsolationLevel::None;
        let b = a;
        assert_eq!(a, b);
        assert_eq!(format!("{a:?}"), "None");
        let mut set = std::collections::HashSet::new();
        set.insert(a);
        assert!(set.contains(&b));
    }

    #[cfg(target_os = "freebsd")]
    mod freebsd_tests {
        use super::*;

        #[test]
        fn capsicum_is_available_on_freebsd() {
            assert!(IsolationLevel::Capsicum.is_available());
        }

        #[test]
        fn capsicum_process_is_available_on_freebsd() {
            assert!(IsolationLevel::CapsicumProcess.is_available());
        }

        #[test]
        fn capsicum_as_str() {
            assert_eq!(IsolationLevel::Capsicum.as_str(), "capsicum");
        }

        #[test]
        fn capsicum_process_as_str() {
            assert_eq!(IsolationLevel::CapsicumProcess.as_str(), "capsicum-process");
        }
    }

    // --- SandboxConfig ----------------------------------------------------

    #[test]
    fn default_has_none_and_capacity_16() {
        let cfg = SandboxConfig::default();
        assert_eq!(cfg.isolation_level(), IsolationLevel::None);
        assert_eq!(cfg.log_channel_capacity(), 16);
    }

    #[test]
    fn builder_sets_isolation_level() {
        let cfg = SandboxConfig::new().with_isolation_level(IsolationLevel::None);
        assert_eq!(cfg.isolation_level(), IsolationLevel::None);
    }

    #[test]
    fn builder_sets_log_channel_capacity() {
        let cfg = SandboxConfig::new().with_log_channel_capacity(64);
        assert_eq!(cfg.log_channel_capacity(), 64);
    }

    #[test]
    fn builder_chains() {
        let cfg = SandboxConfig::new()
            .with_isolation_level(IsolationLevel::None)
            .with_log_channel_capacity(32);
        assert_eq!(cfg.isolation_level(), IsolationLevel::None);
        assert_eq!(cfg.log_channel_capacity(), 32);
    }

    #[test]
    fn platform_supported_for_none() {
        let cfg = SandboxConfig::new().with_isolation_level(IsolationLevel::None);
        assert!(cfg.is_platform_supported());
    }

    #[test]
    fn config_is_copy_clone_eq_debug_hash() {
        let a = SandboxConfig::new();
        let b = a;
        assert_eq!(a, b);
        assert!(format!("{a:?}").contains("SandboxConfig"));
        let mut set = std::collections::HashSet::new();
        set.insert(a);
        assert!(set.contains(&b));
    }

    #[test]
    fn zero_capacity_is_valid() {
        let cfg = SandboxConfig::new().with_log_channel_capacity(0);
        assert_eq!(cfg.log_channel_capacity(), 0);
    }

    #[test]
    fn large_capacity_is_valid() {
        let cfg = SandboxConfig::new().with_log_channel_capacity(usize::MAX);
        assert_eq!(cfg.log_channel_capacity(), usize::MAX);
    }
}