use crate::builder::PluginRegistryBuilder;
pub const API_VERSION: u32 = 1;
pub const PLUGIN_REGISTER_SYMBOL: &str = "razeri_plugin_register_v1";
pub const PLUGIN_API_VERSION_SYMBOL: &str = "razeri_plugin_api_version";
pub type RegisterPluginFn = unsafe fn(&mut PluginRegistryBuilder);
pub type ApiVersionFn = unsafe fn() -> u32;
#[derive(Debug)]
pub enum PluginLoadResult {
Loaded {
name: String,
path: std::path::PathBuf,
},
VersionMismatch {
path: std::path::PathBuf,
expected: u32,
found: u32,
},
Failed {
path: std::path::PathBuf,
error: String,
},
}
#[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
mod loader {
use super::{
API_VERSION, ApiVersionFn, PLUGIN_API_VERSION_SYMBOL, PLUGIN_REGISTER_SYMBOL,
PluginLoadResult, PluginRegistryBuilder, RegisterPluginFn,
};
use std::path::Path;
pub fn load_plugins_from_dir(
dir: &Path,
builder: &mut PluginRegistryBuilder,
) -> Vec<PluginLoadResult> {
warn_if_world_writable(dir);
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(e) => {
tracing::debug!("Cannot read plugin directory {}: {e}", dir.display());
return Vec::new();
}
};
let mut results = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !is_shared_library(&path) {
continue;
}
results.push(load_single_plugin(&path, builder));
}
results
}
fn load_single_plugin(path: &Path, builder: &mut PluginRegistryBuilder) -> PluginLoadResult {
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string();
let lib = match unsafe { libloading::Library::new(path) } {
Ok(l) => l,
Err(e) => {
return PluginLoadResult::Failed {
path: path.to_path_buf(),
error: format!("Failed to load library: {e}"),
};
}
};
let plugin_version = unsafe {
match lib.get::<ApiVersionFn>(PLUGIN_API_VERSION_SYMBOL.as_bytes()) {
Ok(version_fn) => version_fn(),
Err(_) => 1, }
};
if plugin_version != API_VERSION {
return PluginLoadResult::VersionMismatch {
path: path.to_path_buf(),
expected: API_VERSION,
found: plugin_version,
};
}
let register_fn = unsafe {
match lib.get::<RegisterPluginFn>(PLUGIN_REGISTER_SYMBOL.as_bytes()) {
Ok(f) => *f,
Err(e) => {
return PluginLoadResult::Failed {
path: path.to_path_buf(),
error: format!("Missing symbol '{PLUGIN_REGISTER_SYMBOL}': {e}"),
};
}
}
};
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
unsafe { register_fn(builder) };
}));
match result {
Ok(()) => {
tracing::info!("Loaded plugin '{name}' from {}", path.display());
std::mem::forget(lib);
PluginLoadResult::Loaded { name, path: path.to_path_buf() }
}
Err(_) => PluginLoadResult::Failed {
path: path.to_path_buf(),
error: "Plugin registration panicked".to_string(),
},
}
}
fn is_shared_library(path: &Path) -> bool {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
matches!(ext, "so" | "dylib" | "dll")
}
#[cfg(unix)]
fn warn_if_world_writable(dir: &Path) {
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(dir) {
let mode = meta.permissions().mode();
if mode & 0o002 != 0 {
tracing::warn!(
"Plugin directory {} is world-writable — this is a security risk",
dir.display()
);
}
}
}
#[cfg(not(unix))]
fn warn_if_world_writable(_dir: &Path) {
}
}
#[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
pub use loader::load_plugins_from_dir;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_version_is_one() {
assert_eq!(API_VERSION, 1);
}
#[test]
fn symbol_names_are_stable() {
assert_eq!(PLUGIN_REGISTER_SYMBOL, "razeri_plugin_register_v1");
assert_eq!(PLUGIN_API_VERSION_SYMBOL, "razeri_plugin_api_version");
}
#[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
mod dynamic_tests {
use super::*;
use std::path::Path;
#[test]
fn load_from_nonexistent_dir_returns_empty() {
let mut builder = PluginRegistryBuilder::new();
let results = load_plugins_from_dir(
Path::new("/tmp/razeri-nonexistent-plugin-dir-12345"),
&mut builder,
);
assert!(results.is_empty());
}
#[test]
fn load_from_empty_dir_returns_empty() {
let dir = tempfile::tempdir().unwrap();
let mut builder = PluginRegistryBuilder::new();
let results = load_plugins_from_dir(dir.path(), &mut builder);
assert!(results.is_empty());
}
#[test]
fn load_skips_non_library_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("notes.txt"), "hello").unwrap();
std::fs::write(dir.path().join("data.json"), "{}").unwrap();
let mut builder = PluginRegistryBuilder::new();
let results = load_plugins_from_dir(dir.path(), &mut builder);
assert!(results.is_empty());
}
#[test]
fn load_invalid_so_returns_failed() {
let dir = tempfile::tempdir().unwrap();
let lib_name = if cfg!(target_os = "macos") {
"bad.dylib"
} else if cfg!(target_os = "windows") {
"bad.dll"
} else {
"bad.so"
};
std::fs::write(dir.path().join(lib_name), "not a real library").unwrap();
let mut builder = PluginRegistryBuilder::new();
let results = load_plugins_from_dir(dir.path(), &mut builder);
assert_eq!(results.len(), 1);
assert!(
matches!(&results[0], PluginLoadResult::Failed { error, .. } if error.contains("Failed to load library"))
);
}
}
}