Skip to main content

voirs_cli/plugins/
mod.rs

1//! Plugin system for extending VoiRS CLI functionality.
2//!
3//! This module provides a secure, extensible plugin architecture that allows
4//! third-party developers to extend VoiRS with custom effects, voices, and
5//! processing capabilities. The system supports both native Rust plugins
6//! and WebAssembly-based plugins for security and portability.
7//!
8//! ## Features
9//!
10//! - **Secure Plugin Loading**: Sandboxed execution with permission system
11//! - **Multiple Plugin Types**: Effects, voices, processors, and extensions
12//! - **Plugin Discovery**: Automatic discovery from standard directories
13//! - **API Versioning**: Version compatibility checking for plugins
14//! - **Permission System**: Granular control over plugin capabilities
15//! - **Error Handling**: Comprehensive error reporting and recovery
16//!
17//! ## Plugin Types
18//!
19//! - **Effect Plugins**: Audio processing effects (reverb, chorus, etc.)
20//! - **Voice Plugins**: Custom voice models and synthesis engines
21//! - **Processor Plugins**: Text processing and analysis tools
22//! - **Extension Plugins**: General CLI functionality extensions
23//!
24//! ## Example
25//!
26//! ```rust,no_run
27//! use voirs_cli::plugins::PluginManager;
28//!
29//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30//! let mut manager = PluginManager::new();
31//! let plugins = manager.discover_plugins().await?;
32//!
33//! for plugin in plugins {
34//!     println!("Found plugin: {} v{}", plugin.manifest.name, plugin.manifest.version);
35//!     if plugin.enabled {
36//!         manager.load_plugin(&plugin.manifest.name).await?;
37//!     }
38//! }
39//! # Ok(())
40//! # }
41//! ```
42
43use serde::{Deserialize, Serialize};
44use std::collections::HashMap;
45use std::path::{Path, PathBuf};
46use std::sync::Arc;
47use thiserror::Error;
48use tokio::sync::RwLock;
49
50pub mod api;
51pub mod effects;
52pub mod loader;
53pub mod registry;
54pub mod voices;
55
56/// Type alias for the plugin storage map
57type PluginMap = RwLock<HashMap<String, Arc<RwLock<Box<dyn Plugin>>>>>;
58
59#[derive(Debug, Error)]
60pub enum PluginError {
61    #[error("Plugin not found: {0}")]
62    NotFound(String),
63
64    #[error("Plugin loading failed: {0}")]
65    LoadingFailed(String),
66
67    #[error("Invalid plugin manifest: {0}")]
68    InvalidManifest(String),
69
70    #[error("Plugin API version mismatch: expected {expected}, got {actual}")]
71    ApiVersionMismatch { expected: String, actual: String },
72
73    #[error("Plugin permission denied: {0}")]
74    PermissionDenied(String),
75
76    #[error("Plugin execution failed: {0}")]
77    ExecutionFailed(String),
78
79    #[error("Plugin dependency missing: {0}")]
80    DependencyMissing(String),
81
82    #[error("Plugin security violation: {0}")]
83    SecurityViolation(String),
84
85    #[error("IO error: {0}")]
86    IoError(#[from] std::io::Error),
87
88    #[error("Serialization error: {0}")]
89    SerializationError(#[from] serde_json::Error),
90}
91
92pub type PluginResult<T> = Result<T, PluginError>;
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct PluginManifest {
96    pub name: String,
97    pub version: String,
98    pub description: String,
99    pub author: String,
100    pub api_version: String,
101    pub plugin_type: PluginType,
102    pub entry_point: String,
103    pub dependencies: Vec<String>,
104    pub permissions: Vec<Permission>,
105    pub configuration: Option<serde_json::Value>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub enum PluginType {
110    Effect,
111    Voice,
112    Processor,
113    Extension,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum Permission {
118    FileRead,
119    FileWrite,
120    NetworkAccess,
121    SystemInfo,
122    AudioCapture,
123    AudioPlayback,
124    ConfigAccess,
125    ModelAccess,
126}
127
128#[derive(Debug, Clone)]
129pub struct PluginInfo {
130    pub manifest: PluginManifest,
131    pub path: PathBuf,
132    pub loaded: bool,
133    pub enabled: bool,
134    pub load_count: u32,
135    pub last_error: Option<String>,
136}
137
138pub trait Plugin: Send + Sync {
139    fn name(&self) -> &str;
140    fn version(&self) -> &str;
141    fn description(&self) -> &str;
142    fn plugin_type(&self) -> PluginType;
143
144    fn initialize(&mut self, config: &serde_json::Value) -> PluginResult<()>;
145    fn cleanup(&mut self) -> PluginResult<()>;
146
147    fn get_capabilities(&self) -> Vec<String>;
148    fn execute(&self, command: &str, args: &serde_json::Value) -> PluginResult<serde_json::Value>;
149}
150
151pub struct PluginManager {
152    plugins: PluginMap,
153    plugin_info: RwLock<HashMap<String, PluginInfo>>,
154    plugin_directories: Vec<PathBuf>,
155    api_version: String,
156    security_enabled: bool,
157}
158
159impl PluginManager {
160    pub fn new() -> Self {
161        Self {
162            plugins: RwLock::new(HashMap::new()),
163            plugin_info: RwLock::new(HashMap::new()),
164            plugin_directories: vec![
165                dirs::config_dir()
166                    .unwrap_or_default()
167                    .join("voirs")
168                    .join("plugins"),
169                dirs::data_local_dir()
170                    .unwrap_or_default()
171                    .join("voirs")
172                    .join("plugins"),
173                PathBuf::from("/usr/local/share/voirs/plugins"),
174                PathBuf::from("./plugins"),
175            ],
176            api_version: "1.0.0".to_string(),
177            security_enabled: true,
178        }
179    }
180
181    pub fn add_plugin_directory<P: AsRef<Path>>(&mut self, path: P) {
182        self.plugin_directories.push(path.as_ref().to_path_buf());
183    }
184
185    pub async fn discover_plugins(&self) -> PluginResult<Vec<PluginInfo>> {
186        let mut discovered = Vec::new();
187
188        for directory in &self.plugin_directories {
189            if !directory.exists() {
190                continue;
191            }
192
193            let mut entries = tokio::fs::read_dir(directory).await?;
194
195            while let Some(entry) = entries.next_entry().await? {
196                let path = entry.path();
197
198                if path.is_dir() {
199                    let manifest_path = path.join("plugin.json");
200                    if manifest_path.exists() {
201                        match self.load_manifest(&manifest_path).await {
202                            Ok(manifest) => {
203                                let plugin_info = PluginInfo {
204                                    manifest,
205                                    path: path.clone(),
206                                    loaded: false,
207                                    enabled: true,
208                                    load_count: 0,
209                                    last_error: None,
210                                };
211                                discovered.push(plugin_info);
212                            }
213                            Err(e) => {
214                                eprintln!(
215                                    "Failed to load plugin manifest {}: {}",
216                                    manifest_path.display(),
217                                    e
218                                );
219                            }
220                        }
221                    }
222                }
223            }
224        }
225
226        Ok(discovered)
227    }
228
229    pub async fn load_plugin(&self, name: &str) -> PluginResult<()> {
230        let plugin_info = {
231            let info_guard = self.plugin_info.read().await;
232            info_guard
233                .get(name)
234                .cloned()
235                .ok_or_else(|| PluginError::NotFound(name.to_string()))?
236        };
237
238        if plugin_info.manifest.api_version != self.api_version {
239            return Err(PluginError::ApiVersionMismatch {
240                expected: self.api_version.clone(),
241                actual: plugin_info.manifest.api_version.clone(),
242            });
243        }
244
245        if self.security_enabled {
246            self.validate_permissions(&plugin_info.manifest.permissions)?;
247        }
248
249        let plugin = self
250            .load_plugin_from_path(&plugin_info.path, &plugin_info.manifest)
251            .await?;
252
253        {
254            let mut plugins_guard = self.plugins.write().await;
255            plugins_guard.insert(name.to_string(), Arc::new(RwLock::new(plugin)));
256        }
257
258        {
259            let mut info_guard = self.plugin_info.write().await;
260            if let Some(info) = info_guard.get_mut(name) {
261                info.loaded = true;
262                info.load_count += 1;
263                info.last_error = None;
264            }
265        }
266
267        Ok(())
268    }
269
270    pub async fn unload_plugin(&self, name: &str) -> PluginResult<()> {
271        let plugin = {
272            let mut plugins_guard = self.plugins.write().await;
273            plugins_guard
274                .remove(name)
275                .ok_or_else(|| PluginError::NotFound(name.to_string()))?
276        };
277
278        {
279            let mut plugin_guard = plugin.write().await;
280            plugin_guard.cleanup()?;
281        }
282
283        {
284            let mut info_guard = self.plugin_info.write().await;
285            if let Some(info) = info_guard.get_mut(name) {
286                info.loaded = false;
287                info.last_error = None;
288            }
289        }
290
291        Ok(())
292    }
293
294    pub async fn execute_plugin(
295        &self,
296        name: &str,
297        command: &str,
298        args: &serde_json::Value,
299    ) -> PluginResult<serde_json::Value> {
300        let plugin = {
301            let plugins_guard = self.plugins.read().await;
302            plugins_guard
303                .get(name)
304                .cloned()
305                .ok_or_else(|| PluginError::NotFound(name.to_string()))?
306        };
307
308        let plugin_guard = plugin.read().await;
309        plugin_guard.execute(command, args)
310    }
311
312    pub async fn list_plugins(&self) -> Vec<PluginInfo> {
313        let info_guard = self.plugin_info.read().await;
314        info_guard.values().cloned().collect()
315    }
316
317    pub async fn get_plugin_info(&self, name: &str) -> Option<PluginInfo> {
318        let info_guard = self.plugin_info.read().await;
319        info_guard.get(name).cloned()
320    }
321
322    pub async fn enable_plugin(&self, name: &str) -> PluginResult<()> {
323        let mut info_guard = self.plugin_info.write().await;
324        if let Some(info) = info_guard.get_mut(name) {
325            info.enabled = true;
326            Ok(())
327        } else {
328            Err(PluginError::NotFound(name.to_string()))
329        }
330    }
331
332    pub async fn disable_plugin(&self, name: &str) -> PluginResult<()> {
333        let mut info_guard = self.plugin_info.write().await;
334        if let Some(info) = info_guard.get_mut(name) {
335            info.enabled = false;
336            Ok(())
337        } else {
338            Err(PluginError::NotFound(name.to_string()))
339        }
340    }
341
342    async fn load_manifest(&self, path: &Path) -> PluginResult<PluginManifest> {
343        let content = tokio::fs::read_to_string(path).await?;
344        let manifest: PluginManifest = serde_json::from_str(&content)?;
345        Ok(manifest)
346    }
347
348    async fn load_plugin_from_path(
349        &self,
350        _path: &Path,
351        _manifest: &PluginManifest,
352    ) -> PluginResult<Box<dyn Plugin>> {
353        // For now, return a mock plugin
354        // In a real implementation, this would use dynamic loading (libloading crate)
355        // or WebAssembly (wasmtime crate) for security
356        Ok(Box::new(MockPlugin::new()))
357    }
358
359    fn validate_permissions(&self, permissions: &[Permission]) -> PluginResult<()> {
360        // Implement security validation logic
361        for permission in permissions {
362            match permission {
363                Permission::FileWrite => {
364                    // Check if file write is allowed
365                    // This would involve checking system policies, user permissions, etc.
366                }
367                Permission::NetworkAccess => {
368                    // Check if network access is allowed
369                    // This might involve checking firewall rules, network policies, etc.
370                }
371                Permission::SystemInfo => {
372                    // Check if system information access is allowed
373                }
374                _ => {
375                    // Other permissions can be validated here
376                }
377            }
378        }
379        Ok(())
380    }
381}
382
383impl Default for PluginManager {
384    fn default() -> Self {
385        Self::new()
386    }
387}
388
389// Mock plugin for testing and development
390struct MockPlugin {
391    name: String,
392    version: String,
393    description: String,
394}
395
396impl MockPlugin {
397    fn new() -> Self {
398        Self {
399            name: "mock-plugin".to_string(),
400            version: "1.0.0".to_string(),
401            description: "Mock plugin for testing".to_string(),
402        }
403    }
404}
405
406impl Plugin for MockPlugin {
407    fn name(&self) -> &str {
408        &self.name
409    }
410
411    fn version(&self) -> &str {
412        &self.version
413    }
414
415    fn description(&self) -> &str {
416        &self.description
417    }
418
419    fn plugin_type(&self) -> PluginType {
420        PluginType::Extension
421    }
422
423    fn initialize(&mut self, _config: &serde_json::Value) -> PluginResult<()> {
424        Ok(())
425    }
426
427    fn cleanup(&mut self) -> PluginResult<()> {
428        Ok(())
429    }
430
431    fn get_capabilities(&self) -> Vec<String> {
432        vec!["test".to_string()]
433    }
434
435    fn execute(&self, command: &str, args: &serde_json::Value) -> PluginResult<serde_json::Value> {
436        match command {
437            "test" => Ok(serde_json::json!({
438                "status": "ok",
439                "args": args
440            })),
441            _ => Err(PluginError::ExecutionFailed(format!(
442                "Unknown command: {}",
443                command
444            ))),
445        }
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[tokio::test]
454    async fn test_plugin_manager_creation() {
455        let manager = PluginManager::new();
456        assert_eq!(manager.api_version, "1.0.0");
457        assert!(manager.security_enabled);
458    }
459
460    #[tokio::test]
461    async fn test_plugin_discovery() {
462        let manager = PluginManager::new();
463        let plugins = manager.discover_plugins().await.unwrap();
464        // Should not fail even if no plugins found
465        // Plugin discovery should not fail even if no plugins found
466    }
467
468    #[tokio::test]
469    async fn test_mock_plugin() {
470        let plugin = MockPlugin::new();
471        assert_eq!(plugin.name(), "mock-plugin");
472        assert_eq!(plugin.version(), "1.0.0");
473        assert_eq!(plugin.description(), "Mock plugin for testing");
474    }
475
476    #[tokio::test]
477    async fn test_plugin_execution() {
478        let plugin = MockPlugin::new();
479        let result = plugin
480            .execute("test", &serde_json::json!({"key": "value"}))
481            .unwrap();
482        assert_eq!(result["status"], "ok");
483        assert_eq!(result["args"]["key"], "value");
484    }
485
486    #[tokio::test]
487    async fn test_plugin_unknown_command() {
488        let plugin = MockPlugin::new();
489        let result = plugin.execute("unknown", &serde_json::json!({}));
490        assert!(result.is_err());
491    }
492}