Move the files around

This commit is contained in:
Alexander
2026-05-13 20:34:14 +02:00
parent 90e9683076
commit 305d027c8b
113 changed files with 650 additions and 3569 deletions
+42
View File
@@ -0,0 +1,42 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PluginError {
#[error("Plugin not found: {0}")]
NotFound(String),
#[error("Plugin load failed: {0}")]
LoadFailed(String),
#[error("Plugin initialization failed: {0}")]
InitFailed(String),
#[error("Plugin API version mismatch: expected {expected}, got {actual}")]
VersionMismatch { expected: String, actual: String },
#[error("Plugin already loaded: {0}")]
AlreadyLoaded(String),
#[error("Plugin symbol not found: {0}")]
SymbolNotFound(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Plugin execution error: {0}")]
Execution(String),
#[error("Plugin shutdown error: {0}")]
Shutdown(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("WASM error: {0}")]
Wasm(String),
#[error("Resource limit exceeded: {0}")]
ResourceLimit(String),
}
pub type Result<T> = std::result::Result<T, PluginError>;
+15
View File
@@ -0,0 +1,15 @@
pub mod error;
pub mod manager;
pub mod native;
pub mod traits;
pub mod wasm;
pub use error::{PluginError, Result};
pub use manager::{PluginConfig, PluginEntry, PluginManager, WasmConfig};
pub use native::NativePluginHost;
pub use traits::{
ExternalMetadata, FormatPlugin, MetadataPlugin, MetadataQuery, MetadataQueryType,
OriginDirEntry, OriginHealth, OriginInstance, OriginPlugin, OriginStat, Plugin, PluginId,
PluginInfo, PluginType, WatchEvent, WatchHandle, PLUGIN_API_VERSION,
};
pub use wasm::{ResourceLimits, WasmPluginHost};
+346
View File
@@ -0,0 +1,346 @@
use crate::error::{PluginError, Result};
use crate::native::NativePluginHost;
use crate::traits::{Plugin, PluginId, PluginInfo, PluginType};
use crate::wasm::{ResourceLimits, WasmPluginHost};
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::{debug, info};
#[derive(Debug, Clone, Deserialize, Default)]
pub struct PluginConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub search_paths: Vec<PathBuf>,
#[serde(default)]
pub plugins: HashMap<String, PluginEntry>,
#[serde(default)]
pub wasm: WasmConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PluginEntry {
pub path: PathBuf,
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub config: Value,
}
impl Default for PluginEntry {
fn default() -> Self {
Self {
path: PathBuf::new(),
enabled: true,
config: Value::Null,
}
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct WasmConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub max_memory_mb: Option<u32>,
#[serde(default)]
pub max_cpu_time_ms: Option<u32>,
}
pub struct PluginManager {
native_host: NativePluginHost,
wasm_host: WasmPluginHost,
registry: PluginRegistry,
config: PluginConfig,
}
struct PluginRegistry {
origin_plugins: Vec<PluginId>,
metadata_plugins: Vec<PluginId>,
format_plugins: Vec<PluginId>,
}
impl PluginRegistry {
fn new() -> Self {
Self {
origin_plugins: Vec::new(),
metadata_plugins: Vec::new(),
format_plugins: Vec::new(),
}
}
fn register(&mut self, id: PluginId, plugin_type: PluginType) {
match plugin_type {
PluginType::Origin => {
if !self.origin_plugins.contains(&id) {
self.origin_plugins.push(id);
}
}
PluginType::Metadata => {
if !self.metadata_plugins.contains(&id) {
self.metadata_plugins.push(id);
}
}
PluginType::Format => {
if !self.format_plugins.contains(&id) {
self.format_plugins.push(id);
}
}
}
}
fn unregister(&mut self, id: PluginId) {
self.origin_plugins.retain(|&x| x != id);
self.metadata_plugins.retain(|&x| x != id);
self.format_plugins.retain(|&x| x != id);
}
}
impl PluginManager {
pub fn new() -> Result<Self> {
Ok(Self {
native_host: NativePluginHost::new(),
wasm_host: WasmPluginHost::new()?,
registry: PluginRegistry::new(),
config: PluginConfig::default(),
})
}
pub fn init(config: &PluginConfig) -> Result<Self> {
let mut manager = Self::new()?;
manager.config = config.clone();
if !config.enabled {
info!("Plugin system disabled");
return Ok(manager);
}
info!("Initializing plugin system");
for path in &config.search_paths {
manager.native_host.add_search_path(path.clone());
}
if config.wasm.enabled {
let limits = ResourceLimits {
max_memory_mb: config.wasm.max_memory_mb.unwrap_or(64),
max_cpu_time_ms: config.wasm.max_cpu_time_ms.unwrap_or(5000),
..Default::default()
};
manager.wasm_host.set_limits(limits);
}
for (name, entry) in &config.plugins {
if !entry.enabled {
debug!("Skipping disabled plugin: {}", name);
continue;
}
match manager.load_and_init(&entry.path, &entry.config) {
Ok(id) => {
info!("Loaded plugin '{}' with id {:?}", name, id);
}
Err(e) => {
tracing::warn!("Failed to load plugin '{}': {}", name, e);
}
}
}
let discovered = manager.native_host.discover()?;
for id in discovered {
if let Some(info) = manager.native_host.list().iter().find(|i| i.id == id) {
manager.registry.register(id, info.plugin_type);
}
}
Ok(manager)
}
pub fn load_and_init(&mut self, path: &PathBuf, config: &Value) -> Result<PluginId> {
let id = self.native_host.load(path)?;
if let Some(plugin) = self.native_host.get_mut(id) {
plugin.init(config.clone())?;
}
if let Some(info) = self.native_host.list().iter().find(|i| i.id == id) {
self.registry.register(id, info.plugin_type);
}
Ok(id)
}
pub fn load_wasm(&mut self, wasm_bytes: &[u8]) -> Result<PluginId> {
if !self.config.wasm.enabled {
return Err(PluginError::Config("WASM plugins disabled".to_string()));
}
self.wasm_host.load(wasm_bytes)
}
pub fn unload(&mut self, id: PluginId) -> Result<()> {
self.registry.unregister(id);
if let Err(native_err) = self.native_host.unload(id) {
if let Err(wasm_err) = self.wasm_host.unload(id) {
return Err(PluginError::NotFound(format!(
"Plugin {:?} not found in native ({}) or WASM ({}) hosts",
id, native_err, wasm_err
)));
}
}
Ok(())
}
pub fn reload(&mut self, id: PluginId) -> Result<()> {
self.native_host.reload(id)
}
pub fn reload_all(&mut self) -> Result<()> {
let ids: Vec<PluginId> = self.native_host.list().iter().map(|i| i.id).collect();
for id in ids {
self.reload(id)?;
}
Ok(())
}
pub fn list(&self) -> Vec<PluginInfo> {
let mut all = self.native_host.list();
for (id, name) in self.wasm_host.list() {
all.push(PluginInfo {
id,
name: name.to_string(),
version: semver::Version::new(0, 0, 0),
description: "WASM plugin".to_string(),
plugin_type: PluginType::Origin,
});
}
all
}
pub fn get(&self, id: PluginId) -> Option<&dyn Plugin> {
self.native_host.get(id)
}
pub fn get_mut(&mut self, id: PluginId) -> Option<&mut dyn Plugin> {
self.native_host.get_mut(id)
}
pub fn origin_plugin_ids(&self) -> &[PluginId] {
&self.registry.origin_plugins
}
pub fn metadata_plugin_ids(&self) -> &[PluginId] {
&self.registry.metadata_plugins
}
pub fn format_plugin_ids(&self) -> &[PluginId] {
&self.registry.format_plugins
}
pub fn shutdown(&mut self) -> Result<()> {
info!("Shutting down plugin system");
let ids: Vec<PluginId> = self.list().iter().map(|i| i.id).collect();
for id in ids {
if let Err(e) = self.unload(id) {
tracing::warn!("Failed to unload plugin {:?}: {}", id, e);
}
}
Ok(())
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new().expect("Failed to create plugin manager")
}
}
impl Drop for PluginManager {
fn drop(&mut self) {
debug!(plugin_count = self.list().len(), "PluginManager dropping");
let _ = self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_manager_new() {
let manager = PluginManager::new();
assert!(manager.is_ok());
}
#[test]
fn test_plugin_manager_disabled() {
let config = PluginConfig {
enabled: false,
..Default::default()
};
let manager = PluginManager::init(&config);
assert!(manager.is_ok());
let manager = manager.unwrap();
assert!(manager.list().is_empty());
}
#[test]
fn test_registry() {
let mut registry = PluginRegistry::new();
let id1 = PluginId::new(1);
let id2 = PluginId::new(2);
registry.register(id1, PluginType::Origin);
registry.register(id2, PluginType::Metadata);
assert_eq!(registry.origin_plugins.len(), 1);
assert_eq!(registry.metadata_plugins.len(), 1);
registry.unregister(id1);
assert!(registry.origin_plugins.is_empty());
}
#[test]
fn test_plugin_config_deserialize() {
let json = r#"{
"enabled": true,
"search_paths": ["/usr/lib/musicfs/plugins"],
"plugins": {
"example": {
"path": "/path/to/plugin.so",
"enabled": true,
"config": {"key": "value"}
}
},
"wasm": {
"enabled": false
}
}"#;
let config: PluginConfig = serde_json::from_str(json).unwrap();
assert!(config.enabled);
assert_eq!(config.search_paths.len(), 1);
assert!(config.plugins.contains_key("example"));
}
}
+300
View File
@@ -0,0 +1,300 @@
use crate::error::{PluginError, Result};
use crate::traits::{Plugin, PluginId, PluginInfo, PluginType, PLUGIN_API_VERSION};
use libloading::{Library, Symbol};
use semver::Version;
use std::collections::HashMap;
use std::ffi::CStr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use tracing::{debug, info, warn};
static NEXT_PLUGIN_ID: AtomicU64 = AtomicU64::new(1);
fn next_plugin_id() -> PluginId {
PluginId::new(NEXT_PLUGIN_ID.fetch_add(1, Ordering::SeqCst))
}
struct LoadedPlugin {
id: PluginId,
path: PathBuf,
library: Library,
instance: Box<dyn Plugin>,
plugin_type: PluginType,
}
pub struct NativePluginHost {
plugins: HashMap<PluginId, LoadedPlugin>,
search_paths: Vec<PathBuf>,
}
impl NativePluginHost {
pub fn new() -> Self {
Self {
plugins: HashMap::new(),
search_paths: Vec::new(),
}
}
pub fn add_search_path(&mut self, path: PathBuf) {
if !self.search_paths.contains(&path) {
self.search_paths.push(path);
}
}
pub fn load(&mut self, path: &Path) -> Result<PluginId> {
let canonical = path.canonicalize().map_err(|e| {
PluginError::LoadFailed(format!("Cannot resolve path {}: {}", path.display(), e))
})?;
for plugin in self.plugins.values() {
if plugin.path == canonical {
return Err(PluginError::AlreadyLoaded(canonical.display().to_string()));
}
}
info!("Loading native plugin from {:?}", canonical);
let library = unsafe {
Library::new(&canonical)
.map_err(|e| PluginError::LoadFailed(format!("Failed to load library: {}", e)))?
};
self.verify_api_version(&library)?;
let instance = self.create_plugin_instance(&library)?;
let id = next_plugin_id();
let plugin_type = self.detect_plugin_type(&*instance);
debug!(
"Loaded plugin '{}' v{} as {:?}",
instance.name(),
instance.version(),
plugin_type
);
self.plugins.insert(
id,
LoadedPlugin {
id,
path: canonical,
library,
instance,
plugin_type,
},
);
Ok(id)
}
pub fn unload(&mut self, id: PluginId) -> Result<()> {
let mut plugin = self
.plugins
.remove(&id)
.ok_or_else(|| PluginError::NotFound(format!("Plugin {:?}", id)))?;
info!("Unloading plugin '{}'", plugin.instance.name());
plugin.instance.shutdown()?;
drop(plugin.instance);
drop(plugin.library);
Ok(())
}
pub fn reload(&mut self, id: PluginId) -> Result<()> {
let plugin = self
.plugins
.get(&id)
.ok_or_else(|| PluginError::NotFound(format!("Plugin {:?}", id)))?;
let path = plugin.path.clone();
info!("Hot-reloading plugin from {:?}", path);
self.unload(id)?;
let new_id = self.load(&path)?;
if let Some(plugin) = self.plugins.remove(&new_id) {
self.plugins.insert(id, LoadedPlugin { id, ..plugin });
}
Ok(())
}
pub fn get(&self, id: PluginId) -> Option<&dyn Plugin> {
self.plugins.get(&id).map(|p| &*p.instance as &dyn Plugin)
}
pub fn get_mut(&mut self, id: PluginId) -> Option<&mut dyn Plugin> {
self.plugins
.get_mut(&id)
.map(|p| &mut *p.instance as &mut dyn Plugin)
}
pub fn list(&self) -> Vec<PluginInfo> {
self.plugins
.values()
.map(|p| PluginInfo {
id: p.id,
name: p.instance.name().to_string(),
version: p.instance.version(),
description: p.instance.description().to_string(),
plugin_type: p.plugin_type,
})
.collect()
}
pub fn find_by_name(&self, name: &str) -> Option<PluginId> {
self.plugins
.iter()
.find(|(_, p)| p.instance.name() == name)
.map(|(id, _)| *id)
}
pub fn discover(&mut self) -> Result<Vec<PluginId>> {
let mut loaded = Vec::new();
for search_path in self.search_paths.clone() {
if !search_path.exists() {
continue;
}
let entries = std::fs::read_dir(&search_path).map_err(|e| {
PluginError::LoadFailed(format!(
"Cannot read plugin directory {}: {}",
search_path.display(),
e
))
})?;
for entry in entries.flatten() {
let path = entry.path();
if self.is_plugin_library(&path) {
match self.load(&path) {
Ok(id) => loaded.push(id),
Err(e) => {
warn!("Failed to load plugin {:?}: {}", path, e);
}
}
}
}
}
Ok(loaded)
}
fn verify_api_version(&self, library: &Library) -> Result<()> {
let version_fn: Symbol<unsafe extern "C" fn() -> *const std::ffi::c_char> = unsafe {
library.get(b"musicfs_plugin_api_version").map_err(|_| {
PluginError::SymbolNotFound("musicfs_plugin_api_version".to_string())
})?
};
let version_ptr = unsafe { version_fn() };
let version_str = unsafe { CStr::from_ptr(version_ptr) }
.to_str()
.map_err(|_| PluginError::VersionMismatch {
expected: PLUGIN_API_VERSION.to_string(),
actual: "<invalid UTF-8>".to_string(),
})?;
let plugin_version =
Version::parse(version_str).map_err(|_| PluginError::VersionMismatch {
expected: PLUGIN_API_VERSION.to_string(),
actual: version_str.to_string(),
})?;
let expected_version = Version::parse(PLUGIN_API_VERSION).unwrap();
if plugin_version.major != expected_version.major {
return Err(PluginError::VersionMismatch {
expected: PLUGIN_API_VERSION.to_string(),
actual: version_str.to_string(),
});
}
Ok(())
}
fn create_plugin_instance(&self, library: &Library) -> Result<Box<dyn Plugin>> {
let create_fn: Symbol<unsafe extern "C" fn() -> *mut dyn Plugin> = unsafe {
library
.get(b"musicfs_plugin_create")
.map_err(|_| PluginError::SymbolNotFound("musicfs_plugin_create".to_string()))?
};
let plugin_ptr = unsafe { create_fn() };
if plugin_ptr.is_null() {
return Err(PluginError::LoadFailed(
"Plugin factory returned null".to_string(),
));
}
let plugin = unsafe { Box::from_raw(plugin_ptr) };
Ok(plugin)
}
fn detect_plugin_type(&self, plugin: &dyn Plugin) -> PluginType {
plugin.plugin_type()
}
fn is_plugin_library(&self, path: &Path) -> bool {
let extension = path.extension().and_then(|e| e.to_str());
match extension {
Some("so") => true,
Some("dylib") => true,
Some("dll") => true,
_ => false,
}
}
}
impl Default for NativePluginHost {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_native_host_creation() {
let host = NativePluginHost::new();
assert!(host.plugins.is_empty());
assert!(host.search_paths.is_empty());
}
#[test]
fn test_add_search_path() {
let mut host = NativePluginHost::new();
host.add_search_path(PathBuf::from("/usr/lib/musicfs/plugins"));
host.add_search_path(PathBuf::from("/usr/lib/musicfs/plugins"));
assert_eq!(host.search_paths.len(), 1);
}
#[test]
fn test_is_plugin_library() {
let host = NativePluginHost::new();
assert!(host.is_plugin_library(Path::new("plugin.so")));
assert!(host.is_plugin_library(Path::new("plugin.dylib")));
assert!(host.is_plugin_library(Path::new("plugin.dll")));
assert!(!host.is_plugin_library(Path::new("plugin.txt")));
assert!(!host.is_plugin_library(Path::new("plugin")));
}
#[test]
fn test_load_nonexistent() {
let mut host = NativePluginHost::new();
let result = host.load(Path::new("/nonexistent/plugin.so"));
assert!(result.is_err());
}
}
+339
View File
@@ -0,0 +1,339 @@
//! Plugin trait definitions (FR-23.1-23.4)
//!
//! Per architecture.md section 4.3.4:
//! - Plugin trait: Base interface for all plugins
//! - OriginPlugin: Creates Origin instances for storage backends
//! - MetadataPlugin: Provides external metadata lookup
//! - FormatPlugin: Handles custom audio format parsing
use crate::error::Result;
use async_trait::async_trait;
use musicfs_core::AudioMeta;
use semver::Version;
use serde_json::Value;
use std::io::Read;
/// Current plugin API version
pub const PLUGIN_API_VERSION: &str = "0.1.0";
/// Unique identifier for a loaded plugin
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PluginId(pub u64);
impl PluginId {
pub fn new(id: u64) -> Self {
Self(id)
}
}
/// Plugin metadata returned by plugins
#[derive(Debug, Clone)]
pub struct PluginInfo {
pub id: PluginId,
pub name: String,
pub version: Version,
pub description: String,
pub plugin_type: PluginType,
}
/// Type of plugin
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginType {
Origin,
Metadata,
Format,
}
/// Base plugin interface (FR-23.1)
///
/// All plugins must implement this trait. It provides:
/// - Plugin identification (name, version)
/// - Lifecycle management (init, shutdown)
pub trait Plugin: Send + Sync {
/// Unique plugin name (e.g., "s3-origin", "musicbrainz-metadata")
fn name(&self) -> &str;
/// Plugin version following semver
fn version(&self) -> Version;
/// Human-readable description
fn description(&self) -> &str {
""
}
/// Plugin type for registry categorization
fn plugin_type(&self) -> PluginType;
/// Initialize plugin with configuration
///
/// Called once after loading. The config value contains
/// plugin-specific configuration from the main config file.
fn init(&mut self, config: Value) -> Result<()>;
/// Shutdown plugin and release resources
///
/// Called before unloading. Plugins should clean up any
/// resources (connections, file handles, etc).
fn shutdown(&mut self) -> Result<()>;
}
/// Origin plugin interface (FR-23.3)
///
/// Per architecture.md section 4.3.4:
/// Origin plugins create `Box<dyn Origin>` instances for custom storage backends.
///
/// Example use cases:
/// - Google Drive origin
/// - Dropbox origin
/// - Custom NAS protocol
#[async_trait]
pub trait OriginPlugin: Plugin {
/// Origin type identifier (e.g., "gdrive", "dropbox")
fn origin_type(&self) -> &str;
/// Create a new Origin instance with the given configuration
///
/// The config contains origin-specific settings (credentials, paths, etc).
/// Returns a boxed Origin that can be used by the OriginRouter.
async fn create_origin(&self, id: &str, config: Value) -> Result<Box<dyn OriginInstance>>;
}
/// Instance created by OriginPlugin
///
/// This is a simplified async interface that maps to the full Origin trait.
/// The plugin host wraps this to provide the full Origin implementation.
#[async_trait]
pub trait OriginInstance: Send + Sync {
/// List directory contents
async fn readdir(&self, path: &str) -> Result<Vec<OriginDirEntry>>;
/// Get file/directory stats
async fn stat(&self, path: &str) -> Result<OriginStat>;
/// Read file data
async fn read(&self, path: &str, offset: u64, size: u32) -> Result<Vec<u8>>;
/// Check if path exists
async fn exists(&self, path: &str) -> Result<bool>;
/// Health check
async fn health(&self) -> OriginHealth;
/// Watch path for changes (FR-10.2)
async fn watch(
&self,
path: &str,
callback: Box<dyn Fn(WatchEvent) + Send + Sync>,
) -> Result<WatchHandle>;
}
pub struct WatchHandle {
_cancel: tokio::sync::oneshot::Sender<()>,
}
impl WatchHandle {
pub fn new(cancel: tokio::sync::oneshot::Sender<()>) -> Self {
Self { _cancel: cancel }
}
}
#[derive(Debug, Clone)]
pub enum WatchEvent {
Created(String),
Modified(String),
Deleted(String),
}
/// Directory entry from plugin origin
#[derive(Debug, Clone)]
pub struct OriginDirEntry {
pub name: String,
pub is_dir: bool,
pub size: u64,
pub mtime_secs: u64,
}
/// File stats from plugin origin
#[derive(Debug, Clone)]
pub struct OriginStat {
pub size: u64,
pub mtime_secs: u64,
pub is_dir: bool,
}
/// Origin health status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OriginHealth {
Healthy,
Degraded,
Unhealthy,
}
/// Metadata plugin interface (FR-23.3)
///
/// Metadata plugins provide external metadata lookup from services like
/// MusicBrainz, Discogs, Last.fm, etc.
#[async_trait]
pub trait MetadataPlugin: Plugin {
/// Lookup metadata for a query
///
/// Returns enriched metadata if found, None otherwise.
async fn lookup(&self, query: &MetadataQuery) -> Result<Option<ExternalMetadata>>;
/// Supported query types
fn supported_queries(&self) -> &[MetadataQueryType] {
&[MetadataQueryType::ByTitleArtist]
}
}
/// Query for metadata lookup
#[derive(Debug, Clone)]
pub struct MetadataQuery {
pub query_type: MetadataQueryType,
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub fingerprint: Option<String>,
pub duration_ms: Option<u64>,
}
/// Type of metadata query
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetadataQueryType {
ByTitleArtist,
ByFingerprint,
ByAlbum,
}
/// External metadata returned by plugins
#[derive(Debug, Clone, Default)]
pub struct ExternalMetadata {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub album_artist: Option<String>,
pub genre: Option<String>,
pub year: Option<u32>,
pub track: Option<u32>,
pub disc: Option<u32>,
pub musicbrainz_id: Option<String>,
pub artwork_url: Option<String>,
}
/// Format plugin interface (FR-24.1)
///
/// Format plugins handle custom audio formats not supported by symphonia.
///
/// Example use cases:
/// - Custom lossless codecs
/// - Proprietary formats
/// - Game audio formats
pub trait FormatPlugin: Plugin {
/// File extensions this plugin handles
fn extensions(&self) -> &[&str];
/// Check if plugin can handle a specific extension
fn can_handle(&self, extension: &str) -> bool {
self.extensions()
.iter()
.any(|ext| ext.eq_ignore_ascii_case(extension))
}
/// Parse audio metadata from reader
///
/// The reader provides the raw file bytes. Plugin should parse
/// and return AudioMeta with whatever metadata it can extract.
fn parse(&self, reader: &mut dyn Read) -> Result<AudioMeta>;
/// Synthesize file header with updated metadata (FR-5.3)
///
/// Creates a new file header containing the provided metadata.
/// Used for metadata overlay - serving cached metadata without
/// modifying the original file.
fn synthesize_header(&self, metadata: &AudioMeta) -> Result<Vec<u8>>;
}
/// Declaration macro for native plugins
///
/// Native plugins must export a function with this signature:
/// ```ignore
/// #[no_mangle]
/// pub extern "C" fn musicfs_plugin_create() -> *mut dyn Plugin
/// ```
#[macro_export]
macro_rules! declare_plugin {
($plugin_type:ty, $constructor:expr) => {
#[no_mangle]
pub extern "C" fn musicfs_plugin_create() -> *mut dyn $crate::Plugin {
let plugin = $constructor;
let boxed: Box<dyn $crate::Plugin> = Box::new(plugin);
Box::into_raw(boxed)
}
#[no_mangle]
pub extern "C" fn musicfs_plugin_api_version() -> *const std::ffi::c_char {
concat!($crate::PLUGIN_API_VERSION, "\0").as_ptr() as *const std::ffi::c_char
}
};
}
#[cfg(test)]
mod tests {
use super::*;
struct TestPlugin {
name: String,
initialized: bool,
}
impl Plugin for TestPlugin {
fn name(&self) -> &str {
&self.name
}
fn version(&self) -> Version {
Version::new(1, 0, 0)
}
fn plugin_type(&self) -> PluginType {
PluginType::Origin
}
fn init(&mut self, _config: Value) -> Result<()> {
self.initialized = true;
Ok(())
}
fn shutdown(&mut self) -> Result<()> {
self.initialized = false;
Ok(())
}
}
#[test]
fn test_plugin_lifecycle() {
let mut plugin = TestPlugin {
name: "test".to_string(),
initialized: false,
};
assert_eq!(plugin.name(), "test");
assert!(!plugin.initialized);
plugin.init(Value::Null).unwrap();
assert!(plugin.initialized);
plugin.shutdown().unwrap();
assert!(!plugin.initialized);
}
#[test]
fn test_plugin_id() {
let id1 = PluginId::new(1);
let id2 = PluginId::new(1);
let id3 = PluginId::new(2);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
}
+220
View File
@@ -0,0 +1,220 @@
use crate::error::{PluginError, Result};
use crate::traits::PluginId;
#[cfg(feature = "wasm")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "wasm")]
static NEXT_WASM_PLUGIN_ID: AtomicU64 = AtomicU64::new(1_000_000);
#[cfg(feature = "wasm")]
fn next_wasm_plugin_id() -> PluginId {
PluginId::new(NEXT_WASM_PLUGIN_ID.fetch_add(1, Ordering::SeqCst))
}
#[derive(Debug, Clone)]
pub struct ResourceLimits {
pub max_memory_mb: u32,
pub max_cpu_time_ms: u32,
pub allow_network: bool,
pub allow_filesystem: bool,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_memory_mb: 64,
max_cpu_time_ms: 5000,
allow_network: false,
allow_filesystem: false,
}
}
}
#[cfg(feature = "wasm")]
mod wasm_impl {
use super::*;
use std::collections::HashMap;
use tracing::info;
use wasmtime::{Config, Engine, Linker, Module, Store};
pub struct PluginState {
limits: ResourceLimits,
}
pub struct WasmPlugin {
id: PluginId,
name: String,
_module: Module,
}
impl WasmPlugin {
pub fn id(&self) -> PluginId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
}
pub struct WasmPluginHost {
engine: Engine,
linker: Linker<PluginState>,
plugins: HashMap<PluginId, WasmPlugin>,
limits: ResourceLimits,
}
impl WasmPluginHost {
pub fn new() -> Result<Self> {
let mut config = Config::new();
config.consume_fuel(true);
config.epoch_interruption(true);
let engine = Engine::new(&config)
.map_err(|e| PluginError::Wasm(format!("Failed to create WASM engine: {}", e)))?;
let linker = Linker::new(&engine);
Ok(Self {
engine,
linker,
plugins: HashMap::new(),
limits: ResourceLimits::default(),
})
}
pub fn set_limits(&mut self, limits: ResourceLimits) {
self.limits = limits;
}
pub fn load(&mut self, wasm_bytes: &[u8]) -> Result<PluginId> {
info!("Loading WASM plugin ({} bytes)", wasm_bytes.len());
let module = Module::new(&self.engine, wasm_bytes)
.map_err(|e| PluginError::Wasm(format!("Failed to compile WASM module: {}", e)))?;
let id = next_wasm_plugin_id();
let name = module.name().unwrap_or("unnamed").to_string();
let plugin = WasmPlugin {
id,
name,
_module: module,
};
self.plugins.insert(id, plugin);
Ok(id)
}
pub fn unload(&mut self, id: PluginId) -> Result<()> {
self.plugins
.remove(&id)
.ok_or_else(|| PluginError::NotFound(format!("WASM plugin {:?}", id)))?;
Ok(())
}
pub fn get(&self, id: PluginId) -> Option<&WasmPlugin> {
self.plugins.get(&id)
}
pub fn list(&self) -> Vec<(PluginId, &str)> {
self.plugins.iter().map(|(id, p)| (*id, p.name())).collect()
}
fn create_store(&self) -> Store<PluginState> {
let state = PluginState {
limits: self.limits.clone(),
};
let mut store = Store::new(&self.engine, state);
let fuel = (self.limits.max_cpu_time_ms as u64) * 1_000_000;
store.set_fuel(fuel).ok();
store
}
}
impl Default for WasmPluginHost {
fn default() -> Self {
Self::new().expect("Failed to create WASM host")
}
}
}
#[cfg(not(feature = "wasm"))]
mod wasm_stub {
use super::*;
pub struct WasmPluginHost {
limits: ResourceLimits,
}
impl WasmPluginHost {
pub fn new() -> Result<Self> {
Ok(Self {
limits: ResourceLimits::default(),
})
}
pub fn set_limits(&mut self, limits: ResourceLimits) {
self.limits = limits;
}
pub fn load(&mut self, _wasm_bytes: &[u8]) -> Result<PluginId> {
Err(PluginError::Wasm(
"WASM support not enabled. Compile with --features wasm".to_string(),
))
}
pub fn unload(&mut self, _id: PluginId) -> Result<()> {
Err(PluginError::Wasm("WASM support not enabled".to_string()))
}
pub fn list(&self) -> Vec<(PluginId, &str)> {
Vec::new()
}
}
impl Default for WasmPluginHost {
fn default() -> Self {
Self::new().expect("Failed to create WASM stub host")
}
}
}
#[cfg(feature = "wasm")]
pub use wasm_impl::*;
#[cfg(not(feature = "wasm"))]
pub use wasm_stub::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_limits_default() {
let limits = ResourceLimits::default();
assert_eq!(limits.max_memory_mb, 64);
assert_eq!(limits.max_cpu_time_ms, 5000);
assert!(!limits.allow_network);
assert!(!limits.allow_filesystem);
}
#[test]
fn test_wasm_host_creation() {
let host = WasmPluginHost::new();
assert!(host.is_ok());
}
#[test]
#[cfg(not(feature = "wasm"))]
fn test_wasm_disabled_load_fails() {
let mut host = WasmPluginHost::new().unwrap();
let result = host.load(&[0x00, 0x61, 0x73, 0x6d]);
assert!(result.is_err());
}
}