rack 0.4.8

A modern Rust library for hosting audio plugins
Documentation
use crate::{Error, PluginInfo, PluginScanner, PluginType, Result};
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::path::PathBuf;
use std::ptr::NonNull;

use super::ffi;
use super::instance::AudioUnitPlugin;
use super::util::{c_array_to_string, map_error};

/// Scanner for AudioUnit plugins on macOS
///
/// # Thread Safety
///
/// This type is `Send` but not `Sync`:
/// - `Send`: The scanner can be moved between threads safely, as each scanner
///   owns its own C++ state and the AudioComponent APIs are thread-safe for reading.
/// - NOT `Sync`: Multiple threads should not access the scanner simultaneously
///   without synchronization. Wrap in `Arc<Mutex<>>` if shared access is needed.
pub struct AudioUnitScanner {
    inner: NonNull<ffi::RackAUScanner>,
    // PhantomData<*const ()> makes this type !Sync while keeping it Send
    // This prevents concurrent access without Arc<Mutex<>>
    _not_sync: PhantomData<*const ()>,
}

// Safety: AudioUnitScanner can be sent between threads because:
// 1. Each scanner instance owns its C++ state exclusively
// 2. The AudioComponent scanning APIs are thread-safe for reading system state
// 3. No shared mutable state exists between scanner instances
unsafe impl Send for AudioUnitScanner {}

// Note: AudioUnitScanner is NOT Sync due to PhantomData<*const ()>
// This is intentional - the C++ scanner requires synchronization for shared access

impl AudioUnitScanner {
    /// Create a new AudioUnit scanner
    ///
    /// # Errors
    ///
    /// Returns an error if scanner allocation fails
    pub fn new() -> Result<Self> {
        unsafe {
            let ptr = ffi::rack_au_scanner_new();
            if ptr.is_null() {
                return Err(Error::Other("Failed to allocate scanner".to_string()));
            }
            Ok(Self {
                inner: NonNull::new_unchecked(ptr),
                _not_sync: PhantomData,
            })
        }
    }

    /// Scan for AudioUnit components
    fn scan_components(&self) -> Result<Vec<PluginInfo>> {
        unsafe {
            // First pass: get count
            let count = ffi::rack_au_scanner_scan(self.inner.as_ptr(), std::ptr::null_mut(), 0);

            if count < 0 {
                return Err(map_error(count));
            }

            if count == 0 {
                return Ok(Vec::new());
            }

            // Check for integer overflow when converting c_int to usize
            let count_usize = usize::try_from(count)
                .map_err(|_| Error::Other("Plugin count exceeds usize".to_string()))?;

            // Allocate uninitialized array for results
            // Safety: MaybeUninit allows uninitialized memory for C interop
            let mut plugins_c: Vec<MaybeUninit<ffi::RackAUPluginInfo>> =
                Vec::with_capacity(count_usize);
            plugins_c.resize_with(count_usize, MaybeUninit::uninit);

            // Second pass: fill array
            let actual_count = ffi::rack_au_scanner_scan(
                self.inner.as_ptr(),
                plugins_c.as_mut_ptr() as *mut ffi::RackAUPluginInfo,
                count_usize,
            );

            if actual_count < 0 {
                return Err(map_error(actual_count));
            }

            // Handle race condition: plugin list may have changed between passes
            let actual_count_usize = usize::try_from(actual_count)
                .map_err(|_| Error::Other("Actual plugin count exceeds usize".to_string()))?;

            // Use the minimum of the two counts to avoid reading uninitialized memory
            let valid_count = actual_count_usize.min(count_usize);

            // Convert initialized C structs to Rust PluginInfo
            // Safety: The C++ code guarantees that the first `actual_count` elements are initialized
            let plugins = plugins_c
                .into_iter()
                .take(valid_count)
                .map(|p| {
                    // Safety: C++ has written valid data to these elements
                    let plugin_info = p.assume_init();
                    convert_plugin_info(&plugin_info)
                })
                .collect::<Result<Vec<_>>>()?;

            Ok(plugins)
        }
    }
}

/// Convert C plugin info to Rust PluginInfo
fn convert_plugin_info(c_info: &ffi::RackAUPluginInfo) -> Result<PluginInfo> {
    unsafe {
        // Use bounded string conversion for safety
        // This protects against C++ bugs (missing null-termination)
        // by only searching for null within the fixed array bounds
        let name = c_array_to_string(&c_info.name, "plugin name")?;
        let manufacturer = c_array_to_string(&c_info.manufacturer, "manufacturer")?;
        let path_str = c_array_to_string(&c_info.path, "path")?;
        let unique_id = c_array_to_string(&c_info.unique_id, "unique_id")?;

        // Convert plugin type
        let plugin_type = match c_info.plugin_type {
            ffi::RackAUPluginType::Effect => PluginType::Effect,
            ffi::RackAUPluginType::Instrument => PluginType::Instrument,
            ffi::RackAUPluginType::Mixer => PluginType::Mixer,
            ffi::RackAUPluginType::FormatConverter => PluginType::FormatConverter,
            ffi::RackAUPluginType::Other => PluginType::Other,
        };

        Ok(PluginInfo::new(
            name,
            manufacturer,
            c_info.version,
            plugin_type,
            PathBuf::from(path_str),
            unique_id,
        ))
    }
}

impl Drop for AudioUnitScanner {
    fn drop(&mut self) {
        unsafe {
            ffi::rack_au_scanner_free(self.inner.as_ptr());
        }
    }
}

impl PluginScanner for AudioUnitScanner {
    type Plugin = AudioUnitPlugin;

    fn scan(&self) -> Result<Vec<PluginInfo>> {
        self.scan_components()
    }

    fn scan_path(&self, _path: &std::path::Path) -> Result<Vec<PluginInfo>> {
        // AudioUnits are registered with the system, not scanned from paths
        // So we just return the same result as scan()
        self.scan()
    }

    fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
        AudioUnitPlugin::new(info)
    }
}

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

    #[test]
    fn test_scanner_creation() {
        let result = AudioUnitScanner::new();
        assert!(result.is_ok(), "Scanner creation should succeed");
    }

    #[test]
    fn test_scanner_creation_returns_result() {
        // Verify that new() returns Result
        let scanner = AudioUnitScanner::new();
        match scanner {
            Ok(_) => (),
            Err(e) => panic!("Scanner creation failed: {}", e),
        }
    }

    #[test]
    fn test_scan() {
        let scanner = AudioUnitScanner::new().expect("Scanner creation should succeed");
        let result = scanner.scan();
        assert!(result.is_ok(), "Scan should succeed");
    }

    #[test]
    fn test_scan_returns_plugins() {
        let scanner = AudioUnitScanner::new().expect("Scanner creation should succeed");
        let plugins = scanner.scan().expect("Scan should succeed");
        // On macOS, there should always be at least some system AudioUnits
        // But we don't assert a specific count as it varies by system
        println!("Found {} plugins", plugins.len());
    }

    #[test]
    fn test_drop_behavior() {
        // Create and immediately drop scanner to test Drop implementation
        {
            let _scanner = AudioUnitScanner::new().expect("Scanner creation should succeed");
        } // Scanner dropped here
          // If Drop is implemented correctly, this shouldn't leak or crash
    }

    #[test]
    fn test_multiple_scans() {
        let scanner = AudioUnitScanner::new().expect("Scanner creation should succeed");

        // Scan multiple times to ensure it's stable
        let result1 = scanner.scan().expect("First scan should succeed");
        let result2 = scanner.scan().expect("Second scan should succeed");

        // Results should be consistent - both scans should return similar plugin counts
        // On macOS there are always system AudioUnits, so neither should be empty
        // Allow small variance (within 10%) in case plugins are being installed/removed
        let count1 = result1.len();
        let count2 = result2.len();

        assert!(count1 > 0, "First scan should find plugins");
        assert!(count2 > 0, "Second scan should find plugins");

        // Check that counts are reasonably similar (within 20% to be safe)
        let max_count = count1.max(count2);
        let min_count = count1.min(count2);
        let variance = (max_count - min_count) as f64 / max_count as f64;

        assert!(
            variance < 0.2,
            "Scans should be stable: found {} then {} plugins ({}% variance)",
            count1, count2, variance * 100.0
        );
    }

    #[test]
    fn test_plugin_info_fields() {
        let scanner = AudioUnitScanner::new().expect("Scanner creation should succeed");
        let plugins = scanner.scan().expect("Scan should succeed");

        if let Some(plugin) = plugins.first() {
            // Verify all fields are populated
            assert!(!plugin.name.is_empty(), "Plugin name should not be empty");
            assert!(!plugin.manufacturer.is_empty(), "Manufacturer should not be empty");
            assert!(!plugin.unique_id.is_empty(), "Unique ID should not be empty");
            // Version can be 0, so we don't assert it
            // path may be "<system>" for system plugins, so we just check it's not empty
            assert!(plugin.path.as_os_str().len() > 0, "Path should not be empty");
        }
    }

    #[test]
    fn test_scan_path_delegates_to_scan() {
        let scanner = AudioUnitScanner::new().expect("Scanner creation should succeed");
        let path = std::path::Path::new("/dummy/path");

        // scan_path should work for AudioUnits (delegates to scan)
        let result = scanner.scan_path(path);
        assert!(result.is_ok(), "scan_path should succeed");
    }
}