vanguard-plugin 0.1.1

Plugin system for the Vanguard version manager
Documentation
use std::{
    ffi::OsStr,
    path::{Path, PathBuf},
    sync::Arc,
};

use libloading::{Library, Symbol};
use thiserror::Error;

use crate::{CreatePluginFn, VanguardPlugin, CREATE_PLUGIN_SYMBOL};

/// Errors that can occur during dynamic library operations
#[derive(Error, Debug)]
pub enum DylibError {
    /// Failed to load library
    #[error("Failed to load library: {0}")]
    LoadError(#[from] libloading::Error),

    /// Failed to find plugin entry point
    #[error("Failed to find plugin entry point: {0}")]
    EntryPointNotFound(String),

    /// Invalid plugin library
    #[error("Invalid plugin library: {0}")]
    InvalidLibrary(String),
}

/// A loaded dynamic library containing a plugin
pub struct PluginLibrary {
    _lib: Library, // Keep library loaded while we have the plugin
    plugin: Arc<dyn VanguardPlugin>,
}

impl PluginLibrary {
    /// Load a plugin from a dynamic library
    ///
    /// # Safety
    /// This function is unsafe because it loads a dynamic library and calls functions from it.
    /// The caller must ensure that:
    /// - The library path points to a valid dynamic library file
    /// - The library implements the required plugin interface correctly
    /// - The library is compatible with the current platform and architecture
    /// - The library's create_plugin function returns a valid plugin instance
    /// - The library remains loaded for the lifetime of the returned plugin
    pub unsafe fn load<P: AsRef<OsStr>>(path: P) -> Result<Self, DylibError> {
        // Load the dynamic library
        let lib = Library::new(path).map_err(DylibError::LoadError)?;

        // Get the plugin creation function
        let create_plugin: Symbol<CreatePluginFn> = lib
            .get(CREATE_PLUGIN_SYMBOL)
            .map_err(|_| DylibError::EntryPointNotFound("create_plugin".into()))?;

        // Create the plugin instance and get the raw pointer
        let raw_plugin = create_plugin();
        if raw_plugin.is_null() {
            return Err(DylibError::InvalidLibrary(
                "Plugin creation returned null".into(),
            ));
        }

        // Convert the raw plugin back to a Box and extract the instance
        let raw_plugin = Box::from_raw(raw_plugin);
        let plugin = Arc::from(raw_plugin.instance);

        Ok(Self { _lib: lib, plugin })
    }

    /// Get a reference to the loaded plugin
    pub fn plugin(&self) -> Arc<dyn VanguardPlugin> {
        self.plugin.clone()
    }
}

/// Helper to get the dynamic library name for the current platform
pub fn get_dylib_name(name: &str) -> String {
    #[cfg(target_os = "windows")]
    {
        format!("{}.dll", name)
    }
    #[cfg(target_os = "linux")]
    {
        format!("lib{}.so", name)
    }
    #[cfg(target_os = "macos")]
    {
        format!("lib{}.dylib", name)
    }
}

/// Helper to get the dynamic library path for a plugin
pub fn get_dylib_path(plugin_dir: &Path, name: &str) -> PathBuf {
    plugin_dir.join("lib").join(get_dylib_name(name))
}

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

    #[test]
    fn test_dylib_name() {
        let name = get_dylib_name("test-plugin");
        #[cfg(target_os = "windows")]
        assert_eq!(name, "test-plugin.dll");
        #[cfg(target_os = "linux")]
        assert_eq!(name, "libtest-plugin.so");
        #[cfg(target_os = "macos")]
        assert_eq!(name, "libtest-plugin.dylib");
    }

    #[test]
    fn test_dylib_path() {
        let temp_dir = TempDir::new().unwrap();
        let path = get_dylib_path(temp_dir.path(), "test-plugin");
        let expected = temp_dir
            .path()
            .join("lib")
            .join(get_dylib_name("test-plugin"));
        assert_eq!(path, expected);
    }
}