Skip to main content

voirs_cli/plugins/
loader.rs

1//! Plugin loading and dynamic library management.
2
3use super::{Plugin, PluginError, PluginManifest, PluginResult, PluginType};
4use libloading::{Library, Symbol};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::ffi::OsStr;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use wasmtime::{Engine, Instance, Module, Store, TypedFunc};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct LoaderConfig {
14    pub search_paths: Vec<PathBuf>,
15    pub allowed_extensions: Vec<String>,
16    pub security_enabled: bool,
17    pub lazy_loading: bool,
18    pub cache_manifests: bool,
19    pub max_load_attempts: u32,
20    pub load_timeout_ms: u64,
21}
22
23impl Default for LoaderConfig {
24    fn default() -> Self {
25        Self {
26            search_paths: vec![
27                dirs::config_dir()
28                    .unwrap_or_default()
29                    .join("voirs")
30                    .join("plugins"),
31                dirs::data_local_dir()
32                    .unwrap_or_default()
33                    .join("voirs")
34                    .join("plugins"),
35                PathBuf::from("/usr/local/share/voirs/plugins"),
36                PathBuf::from("./plugins"),
37            ],
38            allowed_extensions: vec![
39                "dll".to_string(),   // Windows
40                "so".to_string(),    // Linux
41                "dylib".to_string(), // macOS
42                "wasm".to_string(),  // WebAssembly
43            ],
44            security_enabled: true,
45            lazy_loading: true,
46            cache_manifests: true,
47            max_load_attempts: 3,
48            load_timeout_ms: 5000,
49        }
50    }
51}
52
53pub struct LoadedPlugin {
54    pub manifest: PluginManifest,
55    pub plugin: Arc<dyn Plugin>,
56    pub load_time: std::time::Instant,
57    pub load_count: u32,
58    pub last_access: std::time::Instant,
59    pub plugin_type: LoadedPluginType,
60}
61
62pub enum LoadedPluginType {
63    Native {
64        library: Arc<Library>,
65    },
66    WebAssembly {
67        engine: Arc<Engine>,
68        module: Arc<Module>,
69    },
70    Builtin,
71}
72
73pub struct PluginLoader {
74    config: LoaderConfig,
75    loaded_plugins: HashMap<String, LoadedPlugin>,
76    manifest_cache: HashMap<PathBuf, PluginManifest>,
77    loading_in_progress: HashMap<String, std::time::Instant>,
78    wasm_engine: Arc<Engine>,
79}
80
81impl PluginLoader {
82    pub fn new(config: LoaderConfig) -> PluginResult<Self> {
83        let wasm_engine = Arc::new(Engine::default());
84
85        Ok(Self {
86            config,
87            loaded_plugins: HashMap::new(),
88            manifest_cache: HashMap::new(),
89            loading_in_progress: HashMap::new(),
90            wasm_engine,
91        })
92    }
93
94    pub fn with_default_config() -> PluginResult<Self> {
95        Self::new(LoaderConfig::default())
96    }
97
98    pub async fn discover_plugins(&mut self) -> PluginResult<Vec<PluginManifest>> {
99        let mut discovered = Vec::new();
100        let search_paths = self.config.search_paths.clone();
101
102        for search_path in &search_paths {
103            if !search_path.exists() {
104                continue;
105            }
106
107            let plugins = self.scan_directory(search_path).await?;
108            discovered.extend(plugins);
109        }
110
111        Ok(discovered)
112    }
113
114    async fn scan_directory(&mut self, dir: &Path) -> PluginResult<Vec<PluginManifest>> {
115        let mut plugins = Vec::new();
116        let mut entries = tokio::fs::read_dir(dir).await?;
117
118        while let Some(entry) = entries.next_entry().await? {
119            let path = entry.path();
120
121            if path.is_dir() {
122                // Look for plugin.json in subdirectories
123                let manifest_path = path.join("plugin.json");
124                if manifest_path.exists() {
125                    match self.load_manifest(&manifest_path).await {
126                        Ok(manifest) => plugins.push(manifest),
127                        Err(e) => {
128                            eprintln!(
129                                "Failed to load manifest from {}: {}",
130                                manifest_path.display(),
131                                e
132                            );
133                        }
134                    }
135                }
136            } else if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
137                // Check for plugin libraries
138                if self
139                    .config
140                    .allowed_extensions
141                    .contains(&extension.to_lowercase())
142                {
143                    // Look for accompanying manifest
144                    let manifest_path = path.with_extension("json");
145                    if manifest_path.exists() {
146                        match self.load_manifest(&manifest_path).await {
147                            Ok(manifest) => plugins.push(manifest),
148                            Err(e) => {
149                                eprintln!(
150                                    "Failed to load manifest from {}: {}",
151                                    manifest_path.display(),
152                                    e
153                                );
154                            }
155                        }
156                    }
157                }
158            }
159        }
160
161        Ok(plugins)
162    }
163
164    async fn load_manifest(&mut self, path: &Path) -> PluginResult<PluginManifest> {
165        // Check cache first
166        if self.config.cache_manifests {
167            if let Some(cached) = self.manifest_cache.get(path) {
168                return Ok(cached.clone());
169            }
170        }
171
172        let content = tokio::fs::read_to_string(path).await?;
173        let manifest: PluginManifest = serde_json::from_str(&content)?;
174
175        // Validate manifest
176        self.validate_manifest(&manifest)?;
177
178        // Cache the manifest
179        if self.config.cache_manifests {
180            self.manifest_cache
181                .insert(path.to_path_buf(), manifest.clone());
182        }
183
184        Ok(manifest)
185    }
186
187    fn validate_manifest(&self, manifest: &PluginManifest) -> PluginResult<()> {
188        if manifest.name.is_empty() {
189            return Err(PluginError::InvalidManifest(
190                "Plugin name cannot be empty".to_string(),
191            ));
192        }
193
194        if manifest.version.is_empty() {
195            return Err(PluginError::InvalidManifest(
196                "Plugin version cannot be empty".to_string(),
197            ));
198        }
199
200        if manifest.entry_point.is_empty() {
201            return Err(PluginError::InvalidManifest(
202                "Entry point cannot be empty".to_string(),
203            ));
204        }
205
206        // Validate API version compatibility
207        if !self.is_api_version_compatible(&manifest.api_version) {
208            return Err(PluginError::ApiVersionMismatch {
209                expected: "1.0.x".to_string(),
210                actual: manifest.api_version.clone(),
211            });
212        }
213
214        Ok(())
215    }
216
217    fn is_api_version_compatible(&self, version: &str) -> bool {
218        // Simple semver-like compatibility check
219        // For now, accept 1.0.x versions
220        version.starts_with("1.0.")
221    }
222
223    pub async fn load_plugin(
224        &mut self,
225        name: &str,
226        manifest: &PluginManifest,
227    ) -> PluginResult<Arc<dyn Plugin>> {
228        // Check if already loading
229        if self.loading_in_progress.contains_key(name) {
230            return Err(PluginError::LoadingFailed(format!(
231                "Plugin {} is already being loaded",
232                name
233            )));
234        }
235
236        // Check if already loaded
237        if let Some(loaded) = self.loaded_plugins.get_mut(name) {
238            loaded.last_access = std::time::Instant::now();
239            loaded.load_count += 1;
240            return Ok(loaded.plugin.clone());
241        }
242
243        // Mark as loading
244        self.loading_in_progress
245            .insert(name.to_string(), std::time::Instant::now());
246
247        let result = self.load_plugin_impl(name, manifest).await;
248
249        // Remove from loading queue
250        self.loading_in_progress.remove(name);
251
252        result
253    }
254
255    async fn load_plugin_impl(
256        &mut self,
257        name: &str,
258        manifest: &PluginManifest,
259    ) -> PluginResult<Arc<dyn Plugin>> {
260        let start_time = std::time::Instant::now();
261
262        // Determine plugin type based on entry point
263        let entry_path = self.resolve_plugin_entry_path(manifest)?;
264        let (plugin, plugin_type) = self.load_plugin_from_path(&entry_path, manifest).await?;
265
266        let loaded_plugin = LoadedPlugin {
267            manifest: manifest.clone(),
268            plugin: plugin.clone(),
269            load_time: start_time,
270            load_count: 1,
271            last_access: std::time::Instant::now(),
272            plugin_type,
273        };
274
275        self.loaded_plugins.insert(name.to_string(), loaded_plugin);
276
277        Ok(plugin)
278    }
279
280    fn resolve_plugin_entry_path(&self, manifest: &PluginManifest) -> PluginResult<PathBuf> {
281        // Look for the entry point in the search paths
282        for search_path in &self.config.search_paths {
283            let plugin_dir = search_path.join(&manifest.name);
284            let entry_path = plugin_dir.join(&manifest.entry_point);
285
286            if entry_path.exists() {
287                return Ok(entry_path);
288            }
289
290            // Also check directly in the search path
291            let direct_entry = search_path.join(&manifest.entry_point);
292            if direct_entry.exists() {
293                return Ok(direct_entry);
294            }
295        }
296
297        Err(PluginError::LoadingFailed(format!(
298            "Entry point '{}' not found for plugin '{}'",
299            manifest.entry_point, manifest.name
300        )))
301    }
302
303    async fn load_plugin_from_path(
304        &self,
305        path: &Path,
306        manifest: &PluginManifest,
307    ) -> PluginResult<(Arc<dyn Plugin>, LoadedPluginType)> {
308        let extension = path
309            .extension()
310            .and_then(|ext| ext.to_str())
311            .ok_or_else(|| {
312                PluginError::LoadingFailed("Invalid plugin file extension".to_string())
313            })?;
314
315        match extension.to_lowercase().as_str() {
316            "wasm" => self.load_wasm_plugin(path, manifest).await,
317            "dll" | "so" | "dylib" => self.load_native_plugin(path, manifest).await,
318            _ => {
319                // Fall back to builtin plugin
320                let plugin = self.create_builtin_plugin(manifest)?;
321                Ok((plugin, LoadedPluginType::Builtin))
322            }
323        }
324    }
325
326    async fn load_wasm_plugin(
327        &self,
328        path: &Path,
329        manifest: &PluginManifest,
330    ) -> PluginResult<(Arc<dyn Plugin>, LoadedPluginType)> {
331        let wasm_bytes = tokio::fs::read(path)
332            .await
333            .map_err(|e| PluginError::LoadingFailed(format!("Failed to read WASM file: {}", e)))?;
334
335        let module = Module::new(&self.wasm_engine, &wasm_bytes).map_err(|e| {
336            PluginError::LoadingFailed(format!("Failed to compile WASM module: {}", e))
337        })?;
338
339        let plugin = Arc::new(WasmPlugin::new(
340            manifest.clone(),
341            self.wasm_engine.clone(),
342            Arc::new(module.clone()),
343        ));
344
345        let plugin_type = LoadedPluginType::WebAssembly {
346            engine: self.wasm_engine.clone(),
347            module: Arc::new(module),
348        };
349
350        Ok((plugin as Arc<dyn Plugin>, plugin_type))
351    }
352
353    async fn load_native_plugin(
354        &self,
355        path: &Path,
356        manifest: &PluginManifest,
357    ) -> PluginResult<(Arc<dyn Plugin>, LoadedPluginType)> {
358        let library = unsafe {
359            Library::new(path).map_err(|e| {
360                PluginError::LoadingFailed(format!("Failed to load native library: {}", e))
361            })?
362        };
363
364        let library = Arc::new(library);
365
366        // Look for the plugin factory function
367        let create_plugin: Symbol<unsafe extern "C" fn() -> *mut dyn Plugin> = unsafe {
368            library.get(b"create_plugin").map_err(|e| {
369                PluginError::LoadingFailed(format!(
370                    "Plugin factory function 'create_plugin' not found: {}",
371                    e
372                ))
373            })?
374        };
375
376        let plugin_ptr = unsafe { create_plugin() };
377        if plugin_ptr.is_null() {
378            return Err(PluginError::LoadingFailed(
379                "Plugin factory returned null".to_string(),
380            ));
381        }
382
383        let plugin = unsafe { Arc::from_raw(plugin_ptr) };
384
385        let plugin_type = LoadedPluginType::Native {
386            library: library.clone(),
387        };
388
389        Ok((plugin, plugin_type))
390    }
391
392    fn create_builtin_plugin(&self, manifest: &PluginManifest) -> PluginResult<Arc<dyn Plugin>> {
393        match manifest.plugin_type {
394            PluginType::Effect => Ok(Arc::new(super::effects::ReverbEffectPlugin::new())),
395            PluginType::Voice => Ok(Arc::new(super::voices::DefaultVoicePlugin::new(
396                &manifest.name,
397            ))),
398            PluginType::Processor => Ok(Arc::new(TextProcessorPlugin::new(&manifest.name))),
399            PluginType::Extension => Ok(Arc::new(UtilityExtensionPlugin::new(&manifest.name))),
400        }
401    }
402
403    pub async fn unload_plugin(&mut self, name: &str) -> PluginResult<()> {
404        if let Some(loaded) = self.loaded_plugins.remove(name) {
405            // In a real implementation, this would handle dynamic unloading
406            drop(loaded);
407            Ok(())
408        } else {
409            Err(PluginError::NotFound(name.to_string()))
410        }
411    }
412
413    pub fn is_plugin_loaded(&self, name: &str) -> bool {
414        self.loaded_plugins.contains_key(name)
415    }
416
417    pub fn get_loaded_plugins(&self) -> Vec<String> {
418        self.loaded_plugins.keys().cloned().collect()
419    }
420
421    pub fn get_plugin_info(&self, name: &str) -> Option<&LoadedPlugin> {
422        self.loaded_plugins.get(name)
423    }
424
425    pub fn cleanup_unused_plugins(&mut self, max_idle_time: std::time::Duration) {
426        let now = std::time::Instant::now();
427        self.loaded_plugins
428            .retain(|_name, loaded| now.duration_since(loaded.last_access) < max_idle_time);
429    }
430
431    pub fn get_stats(&self) -> LoaderStats {
432        LoaderStats {
433            total_loaded: self.loaded_plugins.len(),
434            total_cached_manifests: self.manifest_cache.len(),
435            currently_loading: self.loading_in_progress.len(),
436            search_paths: self.config.search_paths.len(),
437        }
438    }
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct LoaderStats {
443    pub total_loaded: usize,
444    pub total_cached_manifests: usize,
445    pub currently_loading: usize,
446    pub search_paths: usize,
447}
448
449// WebAssembly plugin wrapper
450pub struct WasmPlugin {
451    manifest: PluginManifest,
452    engine: Arc<Engine>,
453    module: Arc<Module>,
454}
455
456impl WasmPlugin {
457    pub fn new(manifest: PluginManifest, engine: Arc<Engine>, module: Arc<Module>) -> Self {
458        Self {
459            manifest,
460            engine,
461            module,
462        }
463    }
464
465    fn create_store(&self) -> Store<()> {
466        Store::new(&self.engine, ())
467    }
468
469    fn call_wasm_function(
470        &self,
471        function_name: &str,
472        args: &[wasmtime::Val],
473    ) -> PluginResult<Vec<wasmtime::Val>> {
474        let mut store = self.create_store();
475        let instance = Instance::new(&mut store, &self.module, &[]).map_err(|e| {
476            PluginError::ExecutionFailed(format!("Failed to instantiate WASM module: {}", e))
477        })?;
478
479        let func = instance
480            .get_typed_func::<(), i32>(&mut store, function_name)
481            .map_err(|e| {
482                PluginError::ExecutionFailed(format!(
483                    "Function '{}' not found: {}",
484                    function_name, e
485                ))
486            })?;
487
488        let result = func.call(&mut store, ()).map_err(|e| {
489            PluginError::ExecutionFailed(format!("WASM function call failed: {}", e))
490        })?;
491
492        Ok(vec![wasmtime::Val::I32(result)])
493    }
494}
495
496impl Plugin for WasmPlugin {
497    fn name(&self) -> &str {
498        &self.manifest.name
499    }
500
501    fn version(&self) -> &str {
502        &self.manifest.version
503    }
504
505    fn description(&self) -> &str {
506        &self.manifest.description
507    }
508
509    fn plugin_type(&self) -> PluginType {
510        self.manifest.plugin_type.clone()
511    }
512
513    fn initialize(&mut self, _config: &serde_json::Value) -> PluginResult<()> {
514        // Call WASM initialization function if available
515        match self.call_wasm_function("initialize", &[]) {
516            Ok(_) => Ok(()),
517            Err(_) => {
518                // Initialize function is optional
519                Ok(())
520            }
521        }
522    }
523
524    fn cleanup(&mut self) -> PluginResult<()> {
525        // Call WASM cleanup function if available
526        match self.call_wasm_function("cleanup", &[]) {
527            Ok(_) => Ok(()),
528            Err(_) => {
529                // Cleanup function is optional
530                Ok(())
531            }
532        }
533    }
534
535    fn get_capabilities(&self) -> Vec<String> {
536        // For now, return basic capabilities
537        // In a real implementation, this would query the WASM module
538        vec!["execute".to_string()]
539    }
540
541    fn execute(&self, command: &str, args: &serde_json::Value) -> PluginResult<serde_json::Value> {
542        // For now, return a basic response
543        // In a real implementation, this would call the appropriate WASM function
544        Ok(serde_json::json!({
545            "status": "ok",
546            "command": command,
547            "args": args,
548            "plugin": self.name(),
549            "type": "wasm"
550        }))
551    }
552}
553
554// Default processor plugin implementations
555
556/// Text normalization and preprocessing plugin
557struct TextProcessorPlugin {
558    name: String,
559    normalize_unicode: bool,
560    remove_punctuation: bool,
561    lowercase: bool,
562}
563
564impl TextProcessorPlugin {
565    fn new(name: &str) -> Self {
566        Self {
567            name: format!("processor-{}", name),
568            normalize_unicode: true,
569            remove_punctuation: false,
570            lowercase: false,
571        }
572    }
573
574    fn normalize_text(&self, text: &str) -> String {
575        let mut result = text.to_string();
576
577        // Unicode normalization (NFKC - compatibility normalization)
578        if self.normalize_unicode {
579            result = result
580                .chars()
581                .map(|c| match c {
582                    '\u{FF01}'..='\u{FF5E}' => {
583                        // Convert full-width ASCII to half-width
584                        char::from_u32(c as u32 - 0xFEE0).unwrap_or(c)
585                    }
586                    '\u{3000}' => ' ', // Ideographic space to regular space
587                    _ => c,
588                })
589                .collect();
590        }
591
592        // Remove punctuation
593        if self.remove_punctuation {
594            result = result
595                .chars()
596                .filter(|c| !c.is_ascii_punctuation() && *c != '。' && *c != '、')
597                .collect();
598        }
599
600        // Convert to lowercase
601        if self.lowercase {
602            result = result.to_lowercase();
603        }
604
605        result
606    }
607
608    fn detect_language(&self, text: &str) -> String {
609        // Simple heuristic-based language detection
610        let has_cjk = text.chars().any(|c| {
611            matches!(c,
612                '\u{4E00}'..='\u{9FFF}' | // CJK Unified Ideographs
613                '\u{3040}'..='\u{309F}' | // Hiragana
614                '\u{30A0}'..='\u{30FF}'   // Katakana
615            )
616        });
617
618        let has_hiragana = text.chars().any(|c| matches!(c, '\u{3040}'..='\u{309F}'));
619        let has_hangul = text.chars().any(|c| matches!(c, '\u{AC00}'..='\u{D7AF}'));
620
621        if has_hiragana {
622            "ja".to_string()
623        } else if has_hangul {
624            "ko".to_string()
625        } else if has_cjk {
626            "zh".to_string()
627        } else {
628            "en".to_string()
629        }
630    }
631}
632
633impl Plugin for TextProcessorPlugin {
634    fn name(&self) -> &str {
635        &self.name
636    }
637
638    fn version(&self) -> &str {
639        "1.0.0"
640    }
641
642    fn description(&self) -> &str {
643        "Text normalization and preprocessing plugin"
644    }
645
646    fn plugin_type(&self) -> PluginType {
647        PluginType::Processor
648    }
649
650    fn initialize(&mut self, config: &serde_json::Value) -> PluginResult<()> {
651        if let Some(normalize) = config.get("normalize_unicode").and_then(|v| v.as_bool()) {
652            self.normalize_unicode = normalize;
653        }
654        if let Some(remove_punct) = config.get("remove_punctuation").and_then(|v| v.as_bool()) {
655            self.remove_punctuation = remove_punct;
656        }
657        if let Some(lowercase) = config.get("lowercase").and_then(|v| v.as_bool()) {
658            self.lowercase = lowercase;
659        }
660        Ok(())
661    }
662
663    fn cleanup(&mut self) -> PluginResult<()> {
664        Ok(())
665    }
666
667    fn get_capabilities(&self) -> Vec<String> {
668        vec![
669            "normalize".to_string(),
670            "detect_language".to_string(),
671            "tokenize".to_string(),
672            "clean".to_string(),
673        ]
674    }
675
676    fn execute(&self, command: &str, args: &serde_json::Value) -> PluginResult<serde_json::Value> {
677        match command {
678            "normalize" => {
679                let text = args.get("text").and_then(|v| v.as_str()).ok_or_else(|| {
680                    PluginError::ExecutionFailed("Missing 'text' argument".to_string())
681                })?;
682
683                let normalized = self.normalize_text(text);
684                Ok(serde_json::json!({
685                    "normalized_text": normalized,
686                    "original_length": text.len(),
687                    "normalized_length": normalized.len()
688                }))
689            }
690            "detect_language" => {
691                let text = args.get("text").and_then(|v| v.as_str()).ok_or_else(|| {
692                    PluginError::ExecutionFailed("Missing 'text' argument".to_string())
693                })?;
694
695                let language = self.detect_language(text);
696                Ok(serde_json::json!({
697                    "language": language,
698                    "confidence": 0.85 // Heuristic confidence
699                }))
700            }
701            "tokenize" => {
702                let text = args.get("text").and_then(|v| v.as_str()).ok_or_else(|| {
703                    PluginError::ExecutionFailed("Missing 'text' argument".to_string())
704                })?;
705
706                let tokens: Vec<&str> = text.split_whitespace().collect();
707                Ok(serde_json::json!({
708                    "tokens": tokens,
709                    "token_count": tokens.len()
710                }))
711            }
712            "clean" => {
713                let text = args.get("text").and_then(|v| v.as_str()).ok_or_else(|| {
714                    PluginError::ExecutionFailed("Missing 'text' argument".to_string())
715                })?;
716
717                // Remove extra whitespace, normalize line endings
718                let cleaned = text
719                    .lines()
720                    .map(|line| line.trim())
721                    .filter(|line| !line.is_empty())
722                    .collect::<Vec<_>>()
723                    .join(" ");
724
725                Ok(serde_json::json!({
726                    "cleaned_text": cleaned
727                }))
728            }
729            _ => Err(PluginError::ExecutionFailed(format!(
730                "Unknown command: {}",
731                command
732            ))),
733        }
734    }
735}
736
737/// Utility extension plugin providing common helper functions
738struct UtilityExtensionPlugin {
739    name: String,
740    cache: std::sync::Mutex<HashMap<String, serde_json::Value>>,
741}
742
743impl UtilityExtensionPlugin {
744    fn new(name: &str) -> Self {
745        Self {
746            name: format!("extension-{}", name),
747            cache: std::sync::Mutex::new(HashMap::new()),
748        }
749    }
750
751    fn validate_audio_format(&self, format: &str) -> bool {
752        matches!(
753            format.to_lowercase().as_str(),
754            "wav" | "mp3" | "ogg" | "flac" | "aac" | "opus" | "m4a"
755        )
756    }
757
758    fn convert_duration(&self, duration_str: &str) -> Result<f64, String> {
759        // Parse duration strings like "1:30", "90s", "1.5m"
760        if let Some(colon_pos) = duration_str.find(':') {
761            // Format: "MM:SS"
762            let minutes: f64 = duration_str[..colon_pos]
763                .parse()
764                .map_err(|_| "Invalid minutes")?;
765            let seconds: f64 = duration_str[colon_pos + 1..]
766                .parse()
767                .map_err(|_| "Invalid seconds")?;
768            Ok(minutes * 60.0 + seconds)
769        } else if let Some(stripped) = duration_str.strip_suffix('s') {
770            // Format: "90s"
771            stripped
772                .parse()
773                .map_err(|_| "Invalid seconds value".to_string())
774        } else if let Some(stripped) = duration_str.strip_suffix('m') {
775            // Format: "1.5m"
776            let minutes: f64 = stripped.parse().map_err(|_| "Invalid minutes value")?;
777            Ok(minutes * 60.0)
778        } else if let Some(stripped) = duration_str.strip_suffix('h') {
779            // Format: "0.5h"
780            let hours: f64 = stripped.parse().map_err(|_| "Invalid hours value")?;
781            Ok(hours * 3600.0)
782        } else {
783            // Assume seconds as default
784            duration_str
785                .parse()
786                .map_err(|_| "Invalid duration format".to_string())
787        }
788    }
789
790    fn calculate_audio_bitrate(&self, file_size_bytes: u64, duration_seconds: f64) -> u64 {
791        if duration_seconds > 0.0 {
792            (file_size_bytes * 8) / duration_seconds as u64 / 1000 // kbps
793        } else {
794            0
795        }
796    }
797
798    fn generate_safe_filename(&self, input: &str) -> String {
799        input
800            .chars()
801            .map(|c| {
802                if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
803                    c
804                } else if c.is_whitespace() {
805                    '_'
806                } else {
807                    '-'
808                }
809            })
810            .collect::<String>()
811            .trim_matches('-')
812            .to_string()
813    }
814}
815
816impl Plugin for UtilityExtensionPlugin {
817    fn name(&self) -> &str {
818        &self.name
819    }
820
821    fn version(&self) -> &str {
822        "1.0.0"
823    }
824
825    fn description(&self) -> &str {
826        "Utility extension plugin with helper functions"
827    }
828
829    fn plugin_type(&self) -> PluginType {
830        PluginType::Extension
831    }
832
833    fn initialize(&mut self, _config: &serde_json::Value) -> PluginResult<()> {
834        Ok(())
835    }
836
837    fn cleanup(&mut self) -> PluginResult<()> {
838        if let Ok(mut cache) = self.cache.lock() {
839            cache.clear();
840        }
841        Ok(())
842    }
843
844    fn get_capabilities(&self) -> Vec<String> {
845        vec![
846            "validate_format".to_string(),
847            "convert_duration".to_string(),
848            "calculate_bitrate".to_string(),
849            "safe_filename".to_string(),
850            "cache_get".to_string(),
851            "cache_set".to_string(),
852            "cache_clear".to_string(),
853        ]
854    }
855
856    fn execute(&self, command: &str, args: &serde_json::Value) -> PluginResult<serde_json::Value> {
857        match command {
858            "validate_format" => {
859                let format = args.get("format").and_then(|v| v.as_str()).ok_or_else(|| {
860                    PluginError::ExecutionFailed("Missing 'format' argument".to_string())
861                })?;
862
863                let is_valid = self.validate_audio_format(format);
864                Ok(serde_json::json!({
865                    "valid": is_valid,
866                    "format": format
867                }))
868            }
869            "convert_duration" => {
870                let duration = args
871                    .get("duration")
872                    .and_then(|v| v.as_str())
873                    .ok_or_else(|| {
874                        PluginError::ExecutionFailed("Missing 'duration' argument".to_string())
875                    })?;
876
877                match self.convert_duration(duration) {
878                    Ok(seconds) => Ok(serde_json::json!({
879                        "seconds": seconds,
880                        "minutes": seconds / 60.0,
881                        "hours": seconds / 3600.0
882                    })),
883                    Err(e) => Err(PluginError::ExecutionFailed(e)),
884                }
885            }
886            "calculate_bitrate" => {
887                let file_size =
888                    args.get("file_size")
889                        .and_then(|v| v.as_u64())
890                        .ok_or_else(|| {
891                            PluginError::ExecutionFailed("Missing 'file_size' argument".to_string())
892                        })?;
893                let duration = args
894                    .get("duration")
895                    .and_then(|v| v.as_f64())
896                    .ok_or_else(|| {
897                        PluginError::ExecutionFailed("Missing 'duration' argument".to_string())
898                    })?;
899
900                let bitrate = self.calculate_audio_bitrate(file_size, duration);
901                Ok(serde_json::json!({
902                    "bitrate_kbps": bitrate
903                }))
904            }
905            "safe_filename" => {
906                let filename = args
907                    .get("filename")
908                    .and_then(|v| v.as_str())
909                    .ok_or_else(|| {
910                        PluginError::ExecutionFailed("Missing 'filename' argument".to_string())
911                    })?;
912
913                let safe_name = self.generate_safe_filename(filename);
914                Ok(serde_json::json!({
915                    "safe_filename": safe_name
916                }))
917            }
918            "cache_get" => {
919                let key = args.get("key").and_then(|v| v.as_str()).ok_or_else(|| {
920                    PluginError::ExecutionFailed("Missing 'key' argument".to_string())
921                })?;
922
923                let cache = self.cache.lock().unwrap();
924                Ok(serde_json::json!({
925                    "value": cache.get(key).cloned(),
926                    "exists": cache.contains_key(key)
927                }))
928            }
929            "cache_set" => {
930                let key = args.get("key").and_then(|v| v.as_str()).ok_or_else(|| {
931                    PluginError::ExecutionFailed("Missing 'key' argument".to_string())
932                })?;
933                let value = args.get("value").ok_or_else(|| {
934                    PluginError::ExecutionFailed("Missing 'value' argument".to_string())
935                })?;
936
937                let mut cache = self.cache.lock().unwrap();
938                cache.insert(key.to_string(), value.clone());
939                Ok(serde_json::json!({
940                    "success": true,
941                    "key": key
942                }))
943            }
944            "cache_clear" => {
945                let mut cache = self.cache.lock().unwrap();
946                let count = cache.len();
947                cache.clear();
948                Ok(serde_json::json!({
949                    "cleared": count
950                }))
951            }
952            _ => Err(PluginError::ExecutionFailed(format!(
953                "Unknown command: {}",
954                command
955            ))),
956        }
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    struct MockProcessorPlugin {
965        name: String,
966        version: String,
967        description: String,
968    }
969
970    impl MockProcessorPlugin {
971        fn new(suffix: &str) -> Self {
972            Self {
973                name: format!("processor-{}", suffix),
974                version: "0.1.0".to_string(),
975                description: "Mock processor plugin".to_string(),
976            }
977        }
978    }
979
980    impl Plugin for MockProcessorPlugin {
981        fn name(&self) -> &str {
982            &self.name
983        }
984
985        fn version(&self) -> &str {
986            &self.version
987        }
988
989        fn description(&self) -> &str {
990            &self.description
991        }
992
993        fn plugin_type(&self) -> PluginType {
994            PluginType::Processor
995        }
996
997        fn initialize(&mut self, _config: &serde_json::Value) -> PluginResult<()> {
998            Ok(())
999        }
1000
1001        fn cleanup(&mut self) -> PluginResult<()> {
1002            Ok(())
1003        }
1004
1005        fn get_capabilities(&self) -> Vec<String> {
1006            vec!["mock-processor".to_string()]
1007        }
1008
1009        fn execute(
1010            &self,
1011            _command: &str,
1012            _args: &serde_json::Value,
1013        ) -> PluginResult<serde_json::Value> {
1014            Ok(serde_json::Value::Null)
1015        }
1016    }
1017
1018    struct MockExtensionPlugin {
1019        name: String,
1020        version: String,
1021        description: String,
1022    }
1023
1024    impl MockExtensionPlugin {
1025        fn new(suffix: &str) -> Self {
1026            Self {
1027                name: format!("extension-{}", suffix),
1028                version: "0.1.0".to_string(),
1029                description: "Mock extension plugin".to_string(),
1030            }
1031        }
1032    }
1033
1034    impl Plugin for MockExtensionPlugin {
1035        fn name(&self) -> &str {
1036            &self.name
1037        }
1038
1039        fn version(&self) -> &str {
1040            &self.version
1041        }
1042
1043        fn description(&self) -> &str {
1044            &self.description
1045        }
1046
1047        fn plugin_type(&self) -> PluginType {
1048            PluginType::Extension
1049        }
1050
1051        fn initialize(&mut self, _config: &serde_json::Value) -> PluginResult<()> {
1052            Ok(())
1053        }
1054
1055        fn cleanup(&mut self) -> PluginResult<()> {
1056            Ok(())
1057        }
1058
1059        fn get_capabilities(&self) -> Vec<String> {
1060            vec!["mock-extension".to_string()]
1061        }
1062
1063        fn execute(
1064            &self,
1065            _command: &str,
1066            _args: &serde_json::Value,
1067        ) -> PluginResult<serde_json::Value> {
1068            Ok(serde_json::Value::Null)
1069        }
1070    }
1071
1072    #[test]
1073    fn test_loader_config_default() {
1074        let config = LoaderConfig::default();
1075        assert!(config.security_enabled);
1076        assert!(config.lazy_loading);
1077        assert!(config.cache_manifests);
1078        assert_eq!(config.max_load_attempts, 3);
1079    }
1080
1081    #[tokio::test]
1082    async fn test_plugin_loader_creation() {
1083        let loader = PluginLoader::with_default_config().unwrap();
1084        let stats = loader.get_stats();
1085        assert_eq!(stats.total_loaded, 0);
1086        assert_eq!(stats.currently_loading, 0);
1087    }
1088
1089    #[tokio::test]
1090    async fn test_plugin_discovery() {
1091        let mut loader = PluginLoader::with_default_config().unwrap();
1092        let plugins = loader.discover_plugins().await.unwrap();
1093        // Should not fail even if no plugins found
1094        // Plugin discovery should not fail even if no plugins found
1095    }
1096
1097    #[test]
1098    fn test_manifest_validation() {
1099        let loader = PluginLoader::with_default_config().unwrap();
1100
1101        let valid_manifest = PluginManifest {
1102            name: "test-plugin".to_string(),
1103            version: "1.0.0".to_string(),
1104            description: "Test plugin".to_string(),
1105            author: "Test Author".to_string(),
1106            api_version: "1.0.0".to_string(),
1107            plugin_type: PluginType::Extension,
1108            entry_point: "test_plugin.dll".to_string(),
1109            dependencies: vec![],
1110            permissions: vec![],
1111            configuration: None,
1112        };
1113
1114        assert!(loader.validate_manifest(&valid_manifest).is_ok());
1115
1116        let invalid_manifest = PluginManifest {
1117            name: "".to_string(), // Empty name should fail
1118            ..valid_manifest
1119        };
1120
1121        assert!(loader.validate_manifest(&invalid_manifest).is_err());
1122    }
1123
1124    #[test]
1125    fn test_api_version_compatibility() {
1126        let loader = PluginLoader::with_default_config().unwrap();
1127
1128        assert!(loader.is_api_version_compatible("1.0.0"));
1129        assert!(loader.is_api_version_compatible("1.0.1"));
1130        assert!(!loader.is_api_version_compatible("2.0.0"));
1131        assert!(!loader.is_api_version_compatible("0.9.0"));
1132    }
1133
1134    #[test]
1135    fn test_mock_plugins() {
1136        let processor = MockProcessorPlugin::new("test");
1137        assert_eq!(processor.name(), "processor-test");
1138        assert_eq!(processor.plugin_type(), PluginType::Processor);
1139
1140        let extension = MockExtensionPlugin::new("test");
1141        assert_eq!(extension.name(), "extension-test");
1142        assert_eq!(extension.plugin_type(), PluginType::Extension);
1143    }
1144}