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
//! Plugin-loader errors and the crate [`Result`] alias.

use thiserror::Error;

/// Errors raised while loading, ABI-verifying, or sandboxing an audio plugin.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PluginError {
    /// The plugin's ABI version is incompatible with the host (major differs).
    #[error("ABI version mismatch: host={host:#010x}, plugin={plugin:#010x}")]
    AbiVersionMismatch {
        /// The host's encoded ABI version word.
        host: u32,
        /// The plugin's encoded ABI version word.
        plugin: u32,
    },

    /// A shared library could not be loaded (`dlopen` / `LoadLibrary` failed).
    #[error("failed to load plugin library: {0}")]
    LibraryLoad(String),

    /// A required `extern "C"` symbol was not found in the plugin library.
    #[error("required symbol missing from plugin: {0}")]
    SymbolMissing(String),

    /// A Capsicum / `pdfork` sandbox operation failed (FreeBSD only).
    #[error("sandbox error: {0}")]
    Sandbox(String),

    /// The supplied plugin path was invalid (non-UTF-8, empty, not a file, ...).
    #[error("invalid plugin path: {0}")]
    InvalidPath(String),

    /// An operation was attempted on a plugin that is not currently loaded.
    #[error("plugin is not loaded")]
    NotLoaded,

    /// Plugin metadata failed validation (bad UTF-8, missing required fields, ...).
    #[error("invalid plugin metadata: {0}")]
    InvalidMetadata(String),
}

/// Convenience alias used by consumers of this crate.
pub type Result<T> = core::result::Result<T, PluginError>;

#[cfg(test)]
mod tests {
    use super::{PluginError, Result};

    #[test]
    fn abi_version_mismatch_display_includes_both_words() {
        let msg = PluginError::AbiVersionMismatch {
            host: 0x0001_0000,
            plugin: 0x0002_0000,
        }
        .to_string();
        assert_eq!(
            msg,
            "ABI version mismatch: host=0x00010000, plugin=0x00020000"
        );
    }

    #[test]
    fn library_load_display_includes_detail() {
        let msg = PluginError::LibraryLoad("libfoo.so: undefined symbol".into()).to_string();
        assert_eq!(
            msg,
            "failed to load plugin library: libfoo.so: undefined symbol"
        );
    }

    #[test]
    fn symbol_missing_display_includes_symbol_name() {
        let msg = PluginError::SymbolMissing("audio_plugin_abi_magic".into()).to_string();
        assert_eq!(
            msg,
            "required symbol missing from plugin: audio_plugin_abi_magic"
        );
    }

    #[test]
    fn sandbox_display_includes_detail() {
        let msg = PluginError::Sandbox("cap_enter: not in capability mode".into()).to_string();
        assert_eq!(msg, "sandbox error: cap_enter: not in capability mode");
    }

    #[test]
    fn invalid_path_display_includes_path() {
        let msg = PluginError::InvalidPath("/etc/plugin.so".into()).to_string();
        assert_eq!(msg, "invalid plugin path: /etc/plugin.so");
    }

    #[test]
    fn not_loaded_display_is_human_readable() {
        let msg = PluginError::NotLoaded.to_string();
        assert_eq!(msg, "plugin is not loaded");
    }

    #[test]
    fn invalid_metadata_display_includes_detail() {
        let msg = PluginError::InvalidMetadata("name is empty".into()).to_string();
        assert_eq!(msg, "invalid plugin metadata: name is empty");
    }

    #[test]
    fn result_alias_ok_carries_value() {
        let ok: Result<u32> = Ok(42);
        assert_eq!(ok.map(|v| v + 1), Ok(43));
    }

    #[test]
    fn result_alias_err_carries_plugin_error() {
        let err: Result<u32> = Err(PluginError::NotLoaded);
        assert_eq!(err.err(), Some(PluginError::NotLoaded));
    }

    #[test]
    fn clone_and_partial_eq_hold() {
        let a = PluginError::AbiVersionMismatch { host: 1, plugin: 2 };
        assert_eq!(a.clone(), a);
        // Across-variant inequality (both wrap the integer `1`-ish detail).
        assert_ne!(
            PluginError::LibraryLoad("x".into()),
            PluginError::SymbolMissing("x".into()),
        );
    }
}