1use std::path::Path;
19use std::path::PathBuf;
20
21use mockforge_plugin_core::{
23 PluginAuthor, PluginId, PluginInfo, PluginInstance, PluginManifest, PluginVersion,
24};
25
26pub mod git;
27pub mod installer;
28pub mod loader;
29pub mod metadata;
30pub mod registry;
31pub mod remote;
32pub mod runtime_adapter;
33pub mod sandbox;
34pub mod signature;
35pub mod signature_gen;
36pub mod validator;
37
38pub use git::*;
40pub use installer::*;
41pub use loader::*;
42pub use metadata::*;
43pub use registry::*;
44pub use remote::*;
45pub use runtime_adapter::*;
46pub use sandbox::*;
47pub use signature::*;
48pub use signature_gen::*;
49pub use validator::*;
50
51pub type LoaderResult<T> = std::result::Result<T, PluginLoaderError>;
53
54#[derive(Debug, thiserror::Error)]
56pub enum PluginLoaderError {
57 #[error("Plugin loading error: {message}")]
59 LoadError {
60 message: String,
62 },
63
64 #[error("Plugin validation error: {message}")]
66 ValidationError {
67 message: String,
69 },
70
71 #[error("Security violation: {violation}")]
73 SecurityViolation {
74 violation: String,
76 },
77
78 #[error("Plugin manifest error: {message}")]
80 ManifestError {
81 message: String,
83 },
84
85 #[error("WebAssembly module error: {message}")]
87 WasmError {
88 message: String,
90 },
91
92 #[error("File system error: {message}")]
94 FsError {
95 message: String,
97 },
98
99 #[error("Plugin already loaded: {plugin_id}")]
101 AlreadyLoaded {
102 plugin_id: PluginId,
104 },
105
106 #[error("Plugin not found: {plugin_id}")]
108 NotFound {
109 plugin_id: PluginId,
111 },
112
113 #[error("Plugin dependency error: {message}")]
115 DependencyError {
116 message: String,
118 },
119
120 #[error("Resource limit exceeded: {message}")]
122 ResourceLimit {
123 message: String,
125 },
126
127 #[error("Plugin execution error: {message}")]
129 ExecutionError {
130 message: String,
132 },
133}
134
135impl PluginLoaderError {
136 pub fn load<S: Into<String>>(message: S) -> Self {
138 Self::LoadError {
139 message: message.into(),
140 }
141 }
142
143 pub fn validation<S: Into<String>>(message: S) -> Self {
145 Self::ValidationError {
146 message: message.into(),
147 }
148 }
149
150 pub fn security<S: Into<String>>(violation: S) -> Self {
152 Self::SecurityViolation {
153 violation: violation.into(),
154 }
155 }
156
157 pub fn manifest<S: Into<String>>(message: S) -> Self {
159 Self::ManifestError {
160 message: message.into(),
161 }
162 }
163
164 pub fn wasm<S: Into<String>>(message: S) -> Self {
166 Self::WasmError {
167 message: message.into(),
168 }
169 }
170
171 pub fn fs<S: Into<String>>(message: S) -> Self {
173 Self::FsError {
174 message: message.into(),
175 }
176 }
177
178 pub fn already_loaded(plugin_id: PluginId) -> Self {
180 Self::AlreadyLoaded { plugin_id }
181 }
182
183 pub fn not_found(plugin_id: PluginId) -> Self {
185 Self::NotFound { plugin_id }
186 }
187
188 pub fn dependency<S: Into<String>>(message: S) -> Self {
190 Self::DependencyError {
191 message: message.into(),
192 }
193 }
194
195 pub fn resource_limit<S: Into<String>>(message: S) -> Self {
197 Self::ResourceLimit {
198 message: message.into(),
199 }
200 }
201
202 pub fn execution<S: Into<String>>(message: S) -> Self {
204 Self::ExecutionError {
205 message: message.into(),
206 }
207 }
208
209 pub fn is_security_error(&self) -> bool {
211 matches!(self, PluginLoaderError::SecurityViolation { .. })
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct PluginLoaderConfig {
218 pub plugin_dirs: Vec<String>,
220 pub allow_unsigned: bool,
222 pub trusted_keys: Vec<String>,
224 pub key_data: std::collections::HashMap<String, Vec<u8>>,
226 pub max_plugins: usize,
228 pub load_timeout_secs: u64,
230 pub debug_logging: bool,
232 pub skip_wasm_validation: bool,
234}
235
236impl Default for PluginLoaderConfig {
237 fn default() -> Self {
238 Self {
239 plugin_dirs: vec!["~/.mockforge/plugins".to_string(), "./plugins".to_string()],
240 allow_unsigned: false,
241 trusted_keys: vec!["trusted-dev-key".to_string()],
242 key_data: std::collections::HashMap::new(),
243 max_plugins: 100,
244 load_timeout_secs: 30,
245 debug_logging: false,
246 skip_wasm_validation: false,
247 }
248 }
249}
250
251#[derive(Debug, Clone)]
253pub struct PluginLoadContext {
254 pub plugin_id: PluginId,
256 pub manifest: PluginManifest,
258 pub plugin_path: String,
260 pub load_time: chrono::DateTime<chrono::Utc>,
262 pub config: PluginLoaderConfig,
264}
265
266impl PluginLoadContext {
267 pub fn new(
269 plugin_id: PluginId,
270 manifest: PluginManifest,
271 plugin_path: String,
272 config: PluginLoaderConfig,
273 ) -> Self {
274 Self {
275 plugin_id,
276 manifest,
277 plugin_path,
278 load_time: chrono::Utc::now(),
279 config,
280 }
281 }
282}
283
284#[derive(Debug, Clone, Default)]
286pub struct PluginLoadStats {
287 pub discovered: usize,
289 pub loaded: usize,
291 pub failed: usize,
293 pub skipped: usize,
295 pub start_time: Option<chrono::DateTime<chrono::Utc>>,
297 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
299}
300
301impl PluginLoadStats {
302 pub fn start_loading(&mut self) {
304 self.start_time = Some(chrono::Utc::now());
305 }
306
307 pub fn finish_loading(&mut self) {
309 self.end_time = Some(chrono::Utc::now());
310 }
311
312 pub fn record_success(&mut self) {
314 self.loaded += 1;
315 self.discovered += 1;
316 }
317
318 pub fn record_failure(&mut self) {
320 self.failed += 1;
321 self.discovered += 1;
322 }
323
324 pub fn record_skipped(&mut self) {
326 self.skipped += 1;
327 self.discovered += 1;
328 }
329
330 pub fn duration(&self) -> Option<chrono::Duration> {
332 match (self.start_time, self.end_time) {
333 (Some(start), Some(end)) => Some(end - start),
334 _ => None,
335 }
336 }
337
338 pub fn success_rate(&self) -> f64 {
340 if self.discovered == 0 {
341 1.0 } else {
343 (self.loaded as f64 / self.discovered as f64) * 100.0
344 }
345 }
346
347 pub fn total_plugins(&self) -> usize {
349 self.loaded + self.failed + self.skipped
350 }
351}
352
353#[derive(Debug, Clone)]
355pub struct PluginDiscovery {
356 pub plugin_id: PluginId,
358 pub manifest: PluginManifest,
360 pub path: String,
362 pub is_valid: bool,
364 pub errors: Vec<String>,
366}
367
368impl PluginDiscovery {
369 pub fn success(plugin_id: PluginId, manifest: PluginManifest, path: String) -> Self {
371 Self {
372 plugin_id,
373 manifest,
374 path,
375 is_valid: true,
376 errors: Vec::new(),
377 }
378 }
379
380 pub fn failure(plugin_id: PluginId, path: String, errors: Vec<String>) -> Self {
382 let plugin_id_clone = PluginId(plugin_id.0.clone());
383 Self {
384 plugin_id,
385 manifest: PluginManifest::new(PluginInfo::new(
386 plugin_id_clone,
387 PluginVersion::new(0, 0, 0),
388 "Unknown",
389 "Plugin failed to load",
390 PluginAuthor::new("unknown"),
391 )),
392 path,
393 is_valid: false,
394 errors,
395 }
396 }
397
398 pub fn is_success(&self) -> bool {
400 self.is_valid
401 }
402
403 pub fn first_error(&self) -> Option<&str> {
405 self.errors.first().map(|s| s.as_str())
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
416 fn test_plugin_loader_error_types() {
417 let load_error = PluginLoaderError::LoadError {
418 message: "test error".to_string(),
419 };
420 assert!(matches!(load_error, PluginLoaderError::LoadError { .. }));
421
422 let validation_error = PluginLoaderError::ValidationError {
423 message: "validation failed".to_string(),
424 };
425 assert!(matches!(validation_error, PluginLoaderError::ValidationError { .. }));
426 }
427
428 #[test]
429 fn test_error_helper_constructors() {
430 let load_err = PluginLoaderError::load("load failed");
431 assert!(matches!(load_err, PluginLoaderError::LoadError { .. }));
432
433 let validation_err = PluginLoaderError::validation("validation failed");
434 assert!(matches!(validation_err, PluginLoaderError::ValidationError { .. }));
435
436 let security_err = PluginLoaderError::security("security violation");
437 assert!(matches!(security_err, PluginLoaderError::SecurityViolation { .. }));
438
439 let manifest_err = PluginLoaderError::manifest("manifest error");
440 assert!(matches!(manifest_err, PluginLoaderError::ManifestError { .. }));
441
442 let wasm_err = PluginLoaderError::wasm("wasm error");
443 assert!(matches!(wasm_err, PluginLoaderError::WasmError { .. }));
444
445 let fs_err = PluginLoaderError::fs("fs error");
446 assert!(matches!(fs_err, PluginLoaderError::FsError { .. }));
447
448 let dep_err = PluginLoaderError::dependency("dependency error");
449 assert!(matches!(dep_err, PluginLoaderError::DependencyError { .. }));
450
451 let resource_err = PluginLoaderError::resource_limit("resource limit");
452 assert!(matches!(resource_err, PluginLoaderError::ResourceLimit { .. }));
453
454 let exec_err = PluginLoaderError::execution("execution error");
455 assert!(matches!(exec_err, PluginLoaderError::ExecutionError { .. }));
456 }
457
458 #[test]
459 fn test_error_already_loaded() {
460 let plugin_id = PluginId::new("test-plugin");
461 let err = PluginLoaderError::already_loaded(plugin_id.clone());
462 assert!(matches!(err, PluginLoaderError::AlreadyLoaded { .. }));
463 assert_eq!(err.to_string(), format!("Plugin already loaded: {}", plugin_id));
464 }
465
466 #[test]
467 fn test_error_not_found() {
468 let plugin_id = PluginId::new("missing-plugin");
469 let err = PluginLoaderError::not_found(plugin_id.clone());
470 assert!(matches!(err, PluginLoaderError::NotFound { .. }));
471 assert_eq!(err.to_string(), format!("Plugin not found: {}", plugin_id));
472 }
473
474 #[test]
475 fn test_is_security_error() {
476 let security_err = PluginLoaderError::security("test");
477 assert!(security_err.is_security_error());
478
479 let load_err = PluginLoaderError::load("test");
480 assert!(!load_err.is_security_error());
481 }
482
483 #[test]
484 fn test_error_display() {
485 let err = PluginLoaderError::load("test message");
486 let err_str = err.to_string();
487 assert!(err_str.contains("Plugin loading error"));
488 assert!(err_str.contains("test message"));
489 }
490
491 #[test]
494 fn test_plugin_loader_config_default() {
495 let config = PluginLoaderConfig::default();
496 assert_eq!(config.plugin_dirs.len(), 2);
497 assert!(!config.allow_unsigned);
498 assert_eq!(config.max_plugins, 100);
499 assert_eq!(config.load_timeout_secs, 30);
500 assert!(!config.debug_logging);
501 assert!(!config.skip_wasm_validation);
502 }
503
504 #[test]
505 fn test_plugin_loader_config_clone() {
506 let config = PluginLoaderConfig::default();
507 let cloned = config.clone();
508 assert_eq!(config.max_plugins, cloned.max_plugins);
509 assert_eq!(config.load_timeout_secs, cloned.load_timeout_secs);
510 }
511
512 #[test]
515 fn test_plugin_load_context_creation() {
516 let plugin_id = PluginId::new("test-plugin");
517 let manifest = PluginManifest::new(PluginInfo::new(
518 plugin_id.clone(),
519 PluginVersion::new(1, 0, 0),
520 "Test Plugin",
521 "A test plugin",
522 PluginAuthor::new("test-author"),
523 ));
524 let config = PluginLoaderConfig::default();
525
526 let context = PluginLoadContext::new(
527 plugin_id.clone(),
528 manifest.clone(),
529 "/tmp/plugin".to_string(),
530 config.clone(),
531 );
532
533 assert_eq!(context.plugin_id, plugin_id);
534 assert_eq!(context.plugin_path, "/tmp/plugin");
535 assert_eq!(context.config.max_plugins, config.max_plugins);
536 }
537
538 #[test]
541 fn test_plugin_load_stats_default() {
542 let stats = PluginLoadStats::default();
543 assert_eq!(stats.discovered, 0);
544 assert_eq!(stats.loaded, 0);
545 assert_eq!(stats.failed, 0);
546 assert_eq!(stats.skipped, 0);
547 assert!(stats.start_time.is_none());
548 assert!(stats.end_time.is_none());
549 }
550
551 #[test]
552 fn test_plugin_load_stats_timing() {
553 let mut stats = PluginLoadStats::default();
554 assert!(stats.duration().is_none());
555
556 stats.start_loading();
557 assert!(stats.start_time.is_some());
558 assert!(stats.duration().is_none());
559
560 std::thread::sleep(std::time::Duration::from_millis(10));
561
562 stats.finish_loading();
563 assert!(stats.end_time.is_some());
564 assert!(stats.duration().is_some());
565 let duration = stats.duration().unwrap();
566 assert!(duration.num_milliseconds() >= 10);
567 }
568
569 #[test]
570 fn test_plugin_load_stats_record_success() {
571 let mut stats = PluginLoadStats::default();
572 stats.record_success();
573 assert_eq!(stats.loaded, 1);
574 assert_eq!(stats.discovered, 1);
575
576 stats.record_success();
577 assert_eq!(stats.loaded, 2);
578 assert_eq!(stats.discovered, 2);
579 }
580
581 #[test]
582 fn test_plugin_load_stats_record_failure() {
583 let mut stats = PluginLoadStats::default();
584 stats.record_failure();
585 assert_eq!(stats.failed, 1);
586 assert_eq!(stats.discovered, 1);
587 }
588
589 #[test]
590 fn test_plugin_load_stats_record_skipped() {
591 let mut stats = PluginLoadStats::default();
592 stats.record_skipped();
593 assert_eq!(stats.skipped, 1);
594 assert_eq!(stats.discovered, 1);
595 }
596
597 #[test]
598 fn test_plugin_load_stats_success_rate() {
599 let mut stats = PluginLoadStats::default();
600 assert_eq!(stats.success_rate(), 1.0);
602
603 stats.record_success();
604 stats.record_success();
605 stats.record_failure();
606 stats.record_skipped();
607 assert_eq!(stats.success_rate(), 50.0);
609 }
610
611 #[test]
612 fn test_plugin_load_stats_total_plugins() {
613 let mut stats = PluginLoadStats::default();
614 assert_eq!(stats.total_plugins(), 0);
615
616 stats.record_success();
617 stats.record_failure();
618 stats.record_skipped();
619 assert_eq!(stats.total_plugins(), 3);
620 }
621
622 #[test]
623 fn test_plugin_load_stats_clone() {
624 let mut stats = PluginLoadStats::default();
625 stats.record_success();
626 stats.start_loading();
627
628 let cloned = stats.clone();
629 assert_eq!(cloned.loaded, stats.loaded);
630 assert_eq!(cloned.discovered, stats.discovered);
631 assert_eq!(cloned.start_time, stats.start_time);
632 }
633
634 #[test]
637 fn test_plugin_discovery_success() {
638 let plugin_id = PluginId("test-plugin".to_string());
639 let manifest = PluginManifest::new(PluginInfo::new(
640 plugin_id.clone(),
641 PluginVersion::new(1, 0, 0),
642 "Test Plugin",
643 "A test plugin",
644 PluginAuthor::new("test-author"),
645 ));
646
647 let result = PluginDiscovery::success(plugin_id, manifest, "/path/to/plugin".to_string());
648
649 assert!(result.is_success());
650 assert!(result.first_error().is_none());
651 assert!(result.is_valid);
652 assert!(result.errors.is_empty());
653 }
654
655 #[test]
656 fn test_plugin_discovery_failure() {
657 let plugin_id = PluginId("failing-plugin".to_string());
658 let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
659
660 let result =
661 PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors.clone());
662
663 assert!(!result.is_success());
664 assert_eq!(result.first_error(), Some("Error 1"));
665 assert_eq!(result.errors.len(), 2);
666 assert!(!result.is_valid);
667 }
668
669 #[test]
670 fn test_plugin_discovery_failure_with_empty_errors() {
671 let plugin_id = PluginId("failing-plugin".to_string());
672 let errors = vec![];
673
674 let result = PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors);
675
676 assert!(!result.is_success());
677 assert!(result.first_error().is_none());
678 assert!(result.errors.is_empty());
679 }
680
681 #[test]
682 fn test_plugin_discovery_clone() {
683 let plugin_id = PluginId("test-plugin".to_string());
684 let manifest = PluginManifest::new(PluginInfo::new(
685 plugin_id.clone(),
686 PluginVersion::new(1, 0, 0),
687 "Test",
688 "Test",
689 PluginAuthor::new("test"),
690 ));
691
692 let discovery = PluginDiscovery::success(plugin_id, manifest, "/path".to_string());
693 let cloned = discovery.clone();
694
695 assert_eq!(discovery.plugin_id, cloned.plugin_id);
696 assert_eq!(discovery.is_valid, cloned.is_valid);
697 }
698
699 #[test]
702 fn test_module_exports() {
703 let _ = std::marker::PhantomData::<PluginLoader>;
705 let _ = std::marker::PhantomData::<PluginRegistry>;
706 let _ = std::marker::PhantomData::<PluginValidator>;
707 }
709
710 #[test]
711 fn test_loader_result_type() {
712 let success: LoaderResult<i32> = Ok(42);
713 assert!(success.is_ok());
714 assert_eq!(success.unwrap(), 42);
715
716 let error: LoaderResult<i32> = Err(PluginLoaderError::load("test"));
717 assert!(error.is_err());
718 }
719}