1use 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(), "so".to_string(), "dylib".to_string(), "wasm".to_string(), ],
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 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 if self
139 .config
140 .allowed_extensions
141 .contains(&extension.to_lowercase())
142 {
143 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 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 self.validate_manifest(&manifest)?;
177
178 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 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 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 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 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 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 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 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 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 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 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 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 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
449pub 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 match self.call_wasm_function("initialize", &[]) {
516 Ok(_) => Ok(()),
517 Err(_) => {
518 Ok(())
520 }
521 }
522 }
523
524 fn cleanup(&mut self) -> PluginResult<()> {
525 match self.call_wasm_function("cleanup", &[]) {
527 Ok(_) => Ok(()),
528 Err(_) => {
529 Ok(())
531 }
532 }
533 }
534
535 fn get_capabilities(&self) -> Vec<String> {
536 vec!["execute".to_string()]
539 }
540
541 fn execute(&self, command: &str, args: &serde_json::Value) -> PluginResult<serde_json::Value> {
542 Ok(serde_json::json!({
545 "status": "ok",
546 "command": command,
547 "args": args,
548 "plugin": self.name(),
549 "type": "wasm"
550 }))
551 }
552}
553
554struct 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 if self.normalize_unicode {
579 result = result
580 .chars()
581 .map(|c| match c {
582 '\u{FF01}'..='\u{FF5E}' => {
583 char::from_u32(c as u32 - 0xFEE0).unwrap_or(c)
585 }
586 '\u{3000}' => ' ', _ => c,
588 })
589 .collect();
590 }
591
592 if self.remove_punctuation {
594 result = result
595 .chars()
596 .filter(|c| !c.is_ascii_punctuation() && *c != '。' && *c != '、')
597 .collect();
598 }
599
600 if self.lowercase {
602 result = result.to_lowercase();
603 }
604
605 result
606 }
607
608 fn detect_language(&self, text: &str) -> String {
609 let has_cjk = text.chars().any(|c| {
611 matches!(c,
612 '\u{4E00}'..='\u{9FFF}' | '\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}' )
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 }))
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 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
737struct 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 if let Some(colon_pos) = duration_str.find(':') {
761 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 stripped
772 .parse()
773 .map_err(|_| "Invalid seconds value".to_string())
774 } else if let Some(stripped) = duration_str.strip_suffix('m') {
775 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 let hours: f64 = stripped.parse().map_err(|_| "Invalid hours value")?;
781 Ok(hours * 3600.0)
782 } else {
783 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 } 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 }
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(), ..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}