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
//! Audio-plugin ABI version encoding and compatibility rules.
//!
//! Every plugin `cdylib` must expose two `extern "C"` symbols describing its
//! ABI contract with the host:
//!
//! - `audio_plugin_abi_magic()  -> u32` — must equal
//!   [`AUDIO_PLUGIN_ABI_MAGIC`] (`0x41504C47`, ASCII `"APLG"`).
//! - `audio_plugin_abi_version() -> u32` — a version word encoded with
//!   [`encode_abi_version`]: the **upper 16 bits are the major** version, the
//!   **lower 16 bits are the minor** version.
//!
//! # Compatibility
//!
//! A plugin is ABI-compatible with the host when **both** hold:
//!
//! 1. its magic equals [`AUDIO_PLUGIN_ABI_MAGIC`], and
//! 2. its version **major** equals the host major (see
//!    [`is_abi_compatible`]).
//!
//! The minor version may differ: minor is reserved for additive,
//! backwards-compatible changes (new optional entry symbols, new metadata
//! fields). A major bump is a breaking change and the host will refuse to
//! load the plugin.
//!
//! The magic is a separate symbol from the version word, so the loader checks
//! it independently — compare the plugin's `audio_plugin_abi_magic()` return
//! value against [`AUDIO_PLUGIN_ABI_MAGIC`].

/// Magic word every plugin must return from `audio_plugin_abi_magic()`.
///
/// ASCII `"APLG"` (`0x41 0x50 0x4C 0x47`), chosen to distinguish an audio
/// plugin from an arbitrary shared library that happens to expose a
/// similarly-named symbol.
pub const AUDIO_PLUGIN_ABI_MAGIC: u32 = 0x4150_4C47;

/// ABI version exposed by **this host**: `1.0`.
///
/// Encoded as `(1 << 16) | 0` (see [`encode_abi_version`]). The unit test
/// `host_version_matches_encode_helper` locks this relationship so the
/// constant cannot drift from the encode helper.
pub const AUDIO_PLUGIN_ABI_VERSION: u32 = 1 << 16;

/// Encode a `(major, minor)` pair into a single ABI version word.
///
/// The upper 16 bits hold `major`, the lower 16 bits hold `minor`.
#[must_use]
pub fn encode_abi_version(major: u16, minor: u16) -> u32 {
    (u32::from(major) << 16) | u32::from(minor)
}

/// Decode the **major** component (upper 16 bits) of an encoded ABI version
/// word.
///
/// `version >> 16` is in `0..=u16::MAX` by construction, so the `try_from`
/// always succeeds; `unwrap_or_default` is unreachable and keeps this
/// panic-free (no `as`-cast, no panicking unwrap).
#[must_use]
pub fn abi_major(version: u32) -> u16 {
    u16::try_from(version >> 16).unwrap_or_default()
}

/// Decode the **minor** component (lower 16 bits) of an encoded ABI version
/// word.
///
/// `version & 0xFFFF` is in `0..=u16::MAX` by construction, so the `try_from`
/// always succeeds; `unwrap_or_default` is unreachable and keeps this
/// panic-free.
#[must_use]
pub fn abi_minor(version: u32) -> u16 {
    u16::try_from(version & 0xFFFF).unwrap_or_default()
}

/// Return `true` when `host_version` and `plugin_version` are
/// ABI-compatible, i.e. their **major** components are equal.
///
/// The minor component may differ (additive changes only). The ABI **magic**
/// is checked separately by the loader — compare the plugin's
/// `audio_plugin_abi_magic()` return value against
/// [`AUDIO_PLUGIN_ABI_MAGIC`].
#[must_use]
pub fn is_abi_compatible(host_version: u32, plugin_version: u32) -> bool {
    abi_major(host_version) == abi_major(plugin_version)
}

/// A typed, validated wrapper around an encoded ABI version word.
///
/// Construct with [`AbiVersion::new`] (from `major` / `minor`) or
/// [`AbiVersion::decode`] (from a raw word read off a plugin symbol). It is
/// `Copy`, so it is passed and compared by value.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AbiVersion {
    /// The raw encoded version word (`major << 16 | minor`).
    raw: u32,
}

impl AbiVersion {
    /// Construct an [`AbiVersion`] from explicit `(major, minor)` components.
    #[must_use]
    pub fn new(major: u16, minor: u16) -> Self {
        Self {
            raw: encode_abi_version(major, minor),
        }
    }

    /// Wrap an already-encoded version word without re-validating.
    #[must_use]
    pub fn decode(raw: u32) -> Self {
        Self { raw }
    }

    /// The raw encoded version word.
    #[must_use]
    pub fn raw(self) -> u32 {
        self.raw
    }

    /// The major component (upper 16 bits).
    #[must_use]
    pub fn major(self) -> u16 {
        abi_major(self.raw)
    }

    /// The minor component (lower 16 bits).
    #[must_use]
    pub fn minor(self) -> u16 {
        abi_minor(self.raw)
    }

    /// Return `true` when `self` and `other` share the same **major**
    /// component (i.e. they are ABI-compatible).
    #[must_use]
    pub fn is_compatible_with(self, other: Self) -> bool {
        self.major() == other.major()
    }
}

#[cfg(test)]
mod tests {
    use super::{
        abi_major, abi_minor, encode_abi_version, is_abi_compatible, AbiVersion,
        AUDIO_PLUGIN_ABI_MAGIC, AUDIO_PLUGIN_ABI_VERSION,
    };

    #[test]
    fn host_version_matches_encode_helper() {
        // Locks the host constant against the encode helper.
        assert_eq!(AUDIO_PLUGIN_ABI_VERSION, encode_abi_version(1, 0));
        assert_eq!(abi_major(AUDIO_PLUGIN_ABI_VERSION), 1);
        assert_eq!(abi_minor(AUDIO_PLUGIN_ABI_VERSION), 0);
    }

    #[test]
    fn magic_is_aplg_ascii() {
        // "APLG" big-endian: A=0x41 P=0x50 L=0x4C G=0x47.
        assert_eq!(AUDIO_PLUGIN_ABI_MAGIC, 0x4150_4C47);
    }

    #[test]
    fn encode_decode_roundtrip_typical() {
        let raw = encode_abi_version(3, 7);
        assert_eq!(abi_major(raw), 3);
        assert_eq!(abi_minor(raw), 7);
        assert_eq!(raw, 0x0003_0007);
    }

    #[test]
    fn encode_packs_major_high_minor_low() {
        // major occupies the high half, minor the low half.
        assert_eq!(encode_abi_version(1, 0), 0x0001_0000);
        assert_eq!(encode_abi_version(0, 1), 0x0000_0001);
    }

    #[test]
    fn encode_decode_roundtrip_zero_zero() {
        let raw = encode_abi_version(0, 0);
        assert_eq!(abi_major(raw), 0);
        assert_eq!(abi_minor(raw), 0);
        assert_eq!(raw, 0);
    }

    #[test]
    fn encode_decode_roundtrip_max_max() {
        let raw = encode_abi_version(u16::MAX, u16::MAX);
        assert_eq!(abi_major(raw), u16::MAX);
        assert_eq!(abi_minor(raw), u16::MAX);
        assert_eq!(raw, u32::MAX);
    }

    #[test]
    fn same_major_minor_differs_is_compatible() {
        let host = encode_abi_version(1, 0);
        let plugin = encode_abi_version(1, 9);
        assert!(is_abi_compatible(host, plugin));
    }

    #[test]
    fn major_differs_is_incompatible_even_if_minor_equal() {
        let host = encode_abi_version(1, 5);
        let plugin = encode_abi_version(2, 5);
        assert!(!is_abi_compatible(host, plugin));
    }

    #[test]
    fn identical_versions_are_compatible() {
        let v = encode_abi_version(1, 0);
        assert!(is_abi_compatible(v, v));
    }

    #[test]
    fn is_abi_compatible_is_symmetric() {
        let a = encode_abi_version(1, 2);
        let b = encode_abi_version(1, 9);
        assert_eq!(is_abi_compatible(a, b), is_abi_compatible(b, a));
    }

    #[test]
    fn abi_version_new_roundtrips_through_major_minor() {
        let v = AbiVersion::new(4, 2);
        assert_eq!(v.major(), 4);
        assert_eq!(v.minor(), 2);
        assert_eq!(v.raw(), encode_abi_version(4, 2));
    }

    #[test]
    fn abi_version_decode_preserves_raw() {
        let raw = encode_abi_version(7, 1);
        let v = AbiVersion::decode(raw);
        assert_eq!(v.raw(), raw);
        assert_eq!(v, AbiVersion::new(7, 1));
    }

    #[test]
    fn abi_version_is_compatible_with_major_rule() {
        let host = AbiVersion::new(1, 0);
        assert!(host.is_compatible_with(AbiVersion::new(1, 99)));
        assert!(!host.is_compatible_with(AbiVersion::new(2, 0)));
    }

    #[test]
    fn abi_version_derives_copy_eq_debug() {
        let a = AbiVersion::new(1, 0);
        let b = a; // Copy
        assert_eq!(a, b);
        assert_eq!(format!("{a:?}"), "AbiVersion { raw: 65536 }");
    }

    // --- property tests -------------------------------------------------

    use proptest::prelude::*;

    proptest! {
        #[test]
        fn encode_decode_roundtrip(major in 0u16..=u16::MAX, minor in 0u16..=u16::MAX) {
            let raw = encode_abi_version(major, minor);
            prop_assert_eq!(abi_major(raw), major);
            prop_assert_eq!(abi_minor(raw), minor);
        }

        #[test]
        fn abiversion_new_decode_roundtrip(major in 0u16..=u16::MAX, minor in 0u16..=u16::MAX) {
            let v = AbiVersion::new(major, minor);
            prop_assert_eq!(AbiVersion::decode(v.raw()), v);
            prop_assert_eq!(v.major(), major);
            prop_assert_eq!(v.minor(), minor);
        }

        #[test]
        fn compatibility_depends_only_on_major(
            h_major in 0u16..=u16::MAX,
            h_minor in 0u16..=u16::MAX,
            p_minor in 0u16..=u16::MAX,
        ) {
            let host = encode_abi_version(h_major, h_minor);
            // same major -> always compatible regardless of minor
            let same = encode_abi_version(h_major, p_minor);
            prop_assert!(is_abi_compatible(host, same));
            // different major (when possible) -> never compatible
            let other_major = if h_major == u16::MAX { 0 } else { h_major.wrapping_add(1) };
            if other_major != h_major {
                let diff = encode_abi_version(other_major, p_minor);
                prop_assert!(!is_abi_compatible(host, diff));
            }
        }
    }
}