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 invocation_metrics;
29pub mod loader;
30pub mod memory_tracking;
31pub mod metadata;
32pub mod registry;
33pub mod remote;
34pub mod runtime_adapter;
35pub mod sandbox;
36pub mod signature;
37pub mod signature_gen;
38pub mod validator;
39
40pub use git::*;
42pub use installer::*;
43pub use invocation_metrics::{
44 InvocationMetric, InvocationMetricsBus, InvocationStatus, InvocationTimer,
45};
46pub use loader::*;
47pub use memory_tracking::{MemoryStats, MemoryTracker};
48pub use metadata::*;
49pub use registry::*;
50pub use remote::*;
51pub use runtime_adapter::*;
52pub use sandbox::*;
53pub use signature::*;
54pub use signature_gen::*;
55pub use validator::*;
56
57pub type LoaderResult<T> = Result<T, PluginLoaderError>;
59
60#[derive(Debug, thiserror::Error)]
62pub enum PluginLoaderError {
63 #[error("Plugin loading error: {message}")]
65 LoadError {
66 message: String,
68 },
69
70 #[error("Plugin validation error: {message}")]
72 ValidationError {
73 message: String,
75 },
76
77 #[error("Security violation: {violation}")]
79 SecurityViolation {
80 violation: String,
82 },
83
84 #[error("Plugin manifest error: {message}")]
86 ManifestError {
87 message: String,
89 },
90
91 #[error("WebAssembly module error: {message}")]
93 WasmError {
94 message: String,
96 },
97
98 #[error("File system error: {message}")]
100 FsError {
101 message: String,
103 },
104
105 #[error("Plugin already loaded: {plugin_id}")]
107 AlreadyLoaded {
108 plugin_id: PluginId,
110 },
111
112 #[error("Plugin not found: {plugin_id}")]
114 NotFound {
115 plugin_id: PluginId,
117 },
118
119 #[error("Plugin dependency error: {message}")]
121 DependencyError {
122 message: String,
124 },
125
126 #[error("Resource limit exceeded: {message}")]
128 ResourceLimit {
129 message: String,
131 },
132
133 #[error("Plugin execution error: {message}")]
135 ExecutionError {
136 message: String,
138 },
139}
140
141impl PluginLoaderError {
142 pub fn load<S: Into<String>>(message: S) -> Self {
144 Self::LoadError {
145 message: message.into(),
146 }
147 }
148
149 pub fn validation<S: Into<String>>(message: S) -> Self {
151 Self::ValidationError {
152 message: message.into(),
153 }
154 }
155
156 pub fn security<S: Into<String>>(violation: S) -> Self {
158 Self::SecurityViolation {
159 violation: violation.into(),
160 }
161 }
162
163 pub fn manifest<S: Into<String>>(message: S) -> Self {
165 Self::ManifestError {
166 message: message.into(),
167 }
168 }
169
170 pub fn wasm<S: Into<String>>(message: S) -> Self {
172 Self::WasmError {
173 message: message.into(),
174 }
175 }
176
177 pub fn fs<S: Into<String>>(message: S) -> Self {
179 Self::FsError {
180 message: message.into(),
181 }
182 }
183
184 pub fn already_loaded(plugin_id: PluginId) -> Self {
186 Self::AlreadyLoaded { plugin_id }
187 }
188
189 pub fn not_found(plugin_id: PluginId) -> Self {
191 Self::NotFound { plugin_id }
192 }
193
194 pub fn dependency<S: Into<String>>(message: S) -> Self {
196 Self::DependencyError {
197 message: message.into(),
198 }
199 }
200
201 pub fn resource_limit<S: Into<String>>(message: S) -> Self {
203 Self::ResourceLimit {
204 message: message.into(),
205 }
206 }
207
208 pub fn execution<S: Into<String>>(message: S) -> Self {
210 Self::ExecutionError {
211 message: message.into(),
212 }
213 }
214
215 pub fn is_security_error(&self) -> bool {
217 matches!(self, PluginLoaderError::SecurityViolation { .. })
218 }
219}
220
221#[derive(Debug, Clone)]
223pub struct PluginLoaderConfig {
224 pub plugin_dirs: Vec<String>,
226 pub allow_unsigned: bool,
228 pub trusted_keys: Vec<String>,
230 pub key_data: std::collections::HashMap<String, Vec<u8>>,
232 pub revoked_keys: Vec<String>,
235 pub key_expires_at: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
238 pub max_plugins: usize,
240 pub load_timeout_secs: u64,
242 pub debug_logging: bool,
244 pub skip_wasm_validation: bool,
246}
247
248impl Default for PluginLoaderConfig {
249 fn default() -> Self {
250 Self {
251 plugin_dirs: vec!["~/.mockforge/plugins".to_string(), "./plugins".to_string()],
252 allow_unsigned: false,
253 trusted_keys: vec!["trusted-dev-key".to_string()],
254 key_data: std::collections::HashMap::new(),
255 revoked_keys: Vec::new(),
256 key_expires_at: std::collections::HashMap::new(),
257 max_plugins: 100,
258 load_timeout_secs: 30,
259 debug_logging: false,
260 skip_wasm_validation: false,
261 }
262 }
263}
264
265#[derive(Debug, Clone)]
267pub struct PluginLoadContext {
268 pub plugin_id: PluginId,
270 pub manifest: PluginManifest,
272 pub plugin_path: String,
274 pub load_time: chrono::DateTime<chrono::Utc>,
276 pub config: PluginLoaderConfig,
278}
279
280impl PluginLoadContext {
281 pub fn new(
283 plugin_id: PluginId,
284 manifest: PluginManifest,
285 plugin_path: String,
286 config: PluginLoaderConfig,
287 ) -> Self {
288 Self {
289 plugin_id,
290 manifest,
291 plugin_path,
292 load_time: chrono::Utc::now(),
293 config,
294 }
295 }
296}
297
298#[derive(Debug, Clone, Default)]
300pub struct PluginLoadStats {
301 pub discovered: usize,
303 pub loaded: usize,
305 pub failed: usize,
307 pub skipped: usize,
309 pub start_time: Option<chrono::DateTime<chrono::Utc>>,
311 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
313}
314
315impl PluginLoadStats {
316 pub fn start_loading(&mut self) {
318 self.start_time = Some(chrono::Utc::now());
319 }
320
321 pub fn finish_loading(&mut self) {
323 self.end_time = Some(chrono::Utc::now());
324 }
325
326 pub fn record_success(&mut self) {
328 self.loaded += 1;
329 self.discovered += 1;
330 }
331
332 pub fn record_failure(&mut self) {
334 self.failed += 1;
335 self.discovered += 1;
336 }
337
338 pub fn record_skipped(&mut self) {
340 self.skipped += 1;
341 self.discovered += 1;
342 }
343
344 pub fn duration(&self) -> Option<chrono::Duration> {
346 match (self.start_time, self.end_time) {
347 (Some(start), Some(end)) => Some(end - start),
348 _ => None,
349 }
350 }
351
352 pub fn success_rate(&self) -> f64 {
354 if self.discovered == 0 {
355 1.0 } else {
357 (self.loaded as f64 / self.discovered as f64) * 100.0
358 }
359 }
360
361 pub fn total_plugins(&self) -> usize {
363 self.loaded + self.failed + self.skipped
364 }
365}
366
367#[derive(Debug, Clone)]
369pub struct PluginDiscovery {
370 pub plugin_id: PluginId,
372 pub manifest: PluginManifest,
374 pub path: String,
376 pub is_valid: bool,
378 pub errors: Vec<String>,
380}
381
382impl PluginDiscovery {
383 pub fn success(plugin_id: PluginId, manifest: PluginManifest, path: String) -> Self {
385 Self {
386 plugin_id,
387 manifest,
388 path,
389 is_valid: true,
390 errors: Vec::new(),
391 }
392 }
393
394 pub fn failure(plugin_id: PluginId, path: String, errors: Vec<String>) -> Self {
396 let plugin_id_clone = PluginId(plugin_id.0.clone());
397 Self {
398 plugin_id,
399 manifest: PluginManifest::new(PluginInfo::new(
400 plugin_id_clone,
401 PluginVersion::new(0, 0, 0),
402 "Unknown",
403 "Plugin failed to load",
404 PluginAuthor::new("unknown"),
405 )),
406 path,
407 is_valid: false,
408 errors,
409 }
410 }
411
412 pub fn is_success(&self) -> bool {
414 self.is_valid
415 }
416
417 pub fn first_error(&self) -> Option<&str> {
419 self.errors.first().map(|s| s.as_str())
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
430 fn test_plugin_loader_error_types() {
431 let load_error = PluginLoaderError::LoadError {
432 message: "test error".to_string(),
433 };
434 assert!(matches!(load_error, PluginLoaderError::LoadError { .. }));
435
436 let validation_error = PluginLoaderError::ValidationError {
437 message: "validation failed".to_string(),
438 };
439 assert!(matches!(validation_error, PluginLoaderError::ValidationError { .. }));
440 }
441
442 #[test]
443 fn test_error_helper_constructors() {
444 let load_err = PluginLoaderError::load("load failed");
445 assert!(matches!(load_err, PluginLoaderError::LoadError { .. }));
446
447 let validation_err = PluginLoaderError::validation("validation failed");
448 assert!(matches!(validation_err, PluginLoaderError::ValidationError { .. }));
449
450 let security_err = PluginLoaderError::security("security violation");
451 assert!(matches!(security_err, PluginLoaderError::SecurityViolation { .. }));
452
453 let manifest_err = PluginLoaderError::manifest("manifest error");
454 assert!(matches!(manifest_err, PluginLoaderError::ManifestError { .. }));
455
456 let wasm_err = PluginLoaderError::wasm("wasm error");
457 assert!(matches!(wasm_err, PluginLoaderError::WasmError { .. }));
458
459 let fs_err = PluginLoaderError::fs("fs error");
460 assert!(matches!(fs_err, PluginLoaderError::FsError { .. }));
461
462 let dep_err = PluginLoaderError::dependency("dependency error");
463 assert!(matches!(dep_err, PluginLoaderError::DependencyError { .. }));
464
465 let resource_err = PluginLoaderError::resource_limit("resource limit");
466 assert!(matches!(resource_err, PluginLoaderError::ResourceLimit { .. }));
467
468 let exec_err = PluginLoaderError::execution("execution error");
469 assert!(matches!(exec_err, PluginLoaderError::ExecutionError { .. }));
470 }
471
472 #[test]
473 fn test_error_already_loaded() {
474 let plugin_id = PluginId::new("test-plugin");
475 let err = PluginLoaderError::already_loaded(plugin_id.clone());
476 assert!(matches!(err, PluginLoaderError::AlreadyLoaded { .. }));
477 assert_eq!(err.to_string(), format!("Plugin already loaded: {}", plugin_id));
478 }
479
480 #[test]
481 fn test_error_not_found() {
482 let plugin_id = PluginId::new("missing-plugin");
483 let err = PluginLoaderError::not_found(plugin_id.clone());
484 assert!(matches!(err, PluginLoaderError::NotFound { .. }));
485 assert_eq!(err.to_string(), format!("Plugin not found: {}", plugin_id));
486 }
487
488 #[test]
489 fn test_is_security_error() {
490 let security_err = PluginLoaderError::security("test");
491 assert!(security_err.is_security_error());
492
493 let load_err = PluginLoaderError::load("test");
494 assert!(!load_err.is_security_error());
495 }
496
497 #[test]
498 fn test_error_display() {
499 let err = PluginLoaderError::load("test message");
500 let err_str = err.to_string();
501 assert!(err_str.contains("Plugin loading error"));
502 assert!(err_str.contains("test message"));
503 }
504
505 #[test]
508 fn test_plugin_loader_config_default() {
509 let config = PluginLoaderConfig::default();
510 assert_eq!(config.plugin_dirs.len(), 2);
511 assert!(!config.allow_unsigned);
512 assert_eq!(config.max_plugins, 100);
513 assert_eq!(config.load_timeout_secs, 30);
514 assert!(!config.debug_logging);
515 assert!(!config.skip_wasm_validation);
516 }
517
518 #[test]
519 fn test_plugin_loader_config_clone() {
520 let config = PluginLoaderConfig::default();
521 let cloned = config.clone();
522 assert_eq!(config.max_plugins, cloned.max_plugins);
523 assert_eq!(config.load_timeout_secs, cloned.load_timeout_secs);
524 }
525
526 #[test]
529 fn test_plugin_load_context_creation() {
530 let plugin_id = PluginId::new("test-plugin");
531 let manifest = PluginManifest::new(PluginInfo::new(
532 plugin_id.clone(),
533 PluginVersion::new(1, 0, 0),
534 "Test Plugin",
535 "A test plugin",
536 PluginAuthor::new("test-author"),
537 ));
538 let config = PluginLoaderConfig::default();
539
540 let context = PluginLoadContext::new(
541 plugin_id.clone(),
542 manifest.clone(),
543 "/tmp/plugin".to_string(),
544 config.clone(),
545 );
546
547 assert_eq!(context.plugin_id, plugin_id);
548 assert_eq!(context.plugin_path, "/tmp/plugin");
549 assert_eq!(context.config.max_plugins, config.max_plugins);
550 }
551
552 #[test]
555 fn test_plugin_load_stats_default() {
556 let stats = PluginLoadStats::default();
557 assert_eq!(stats.discovered, 0);
558 assert_eq!(stats.loaded, 0);
559 assert_eq!(stats.failed, 0);
560 assert_eq!(stats.skipped, 0);
561 assert!(stats.start_time.is_none());
562 assert!(stats.end_time.is_none());
563 }
564
565 #[test]
566 fn test_plugin_load_stats_timing() {
567 let mut stats = PluginLoadStats::default();
568 assert!(stats.duration().is_none());
569
570 stats.start_loading();
571 assert!(stats.start_time.is_some());
572 assert!(stats.duration().is_none());
573
574 std::thread::sleep(std::time::Duration::from_millis(10));
575
576 stats.finish_loading();
577 assert!(stats.end_time.is_some());
578 assert!(stats.duration().is_some());
579 let duration = stats.duration().unwrap();
580 assert!(duration.num_milliseconds() >= 10);
581 }
582
583 #[test]
584 fn test_plugin_load_stats_record_success() {
585 let mut stats = PluginLoadStats::default();
586 stats.record_success();
587 assert_eq!(stats.loaded, 1);
588 assert_eq!(stats.discovered, 1);
589
590 stats.record_success();
591 assert_eq!(stats.loaded, 2);
592 assert_eq!(stats.discovered, 2);
593 }
594
595 #[test]
596 fn test_plugin_load_stats_record_failure() {
597 let mut stats = PluginLoadStats::default();
598 stats.record_failure();
599 assert_eq!(stats.failed, 1);
600 assert_eq!(stats.discovered, 1);
601 }
602
603 #[test]
604 fn test_plugin_load_stats_record_skipped() {
605 let mut stats = PluginLoadStats::default();
606 stats.record_skipped();
607 assert_eq!(stats.skipped, 1);
608 assert_eq!(stats.discovered, 1);
609 }
610
611 #[test]
612 fn test_plugin_load_stats_success_rate() {
613 let mut stats = PluginLoadStats::default();
614 assert_eq!(stats.success_rate(), 1.0);
616
617 stats.record_success();
618 stats.record_success();
619 stats.record_failure();
620 stats.record_skipped();
621 assert_eq!(stats.success_rate(), 50.0);
623 }
624
625 #[test]
626 fn test_plugin_load_stats_total_plugins() {
627 let mut stats = PluginLoadStats::default();
628 assert_eq!(stats.total_plugins(), 0);
629
630 stats.record_success();
631 stats.record_failure();
632 stats.record_skipped();
633 assert_eq!(stats.total_plugins(), 3);
634 }
635
636 #[test]
637 fn test_plugin_load_stats_clone() {
638 let mut stats = PluginLoadStats::default();
639 stats.record_success();
640 stats.start_loading();
641
642 let cloned = stats.clone();
643 assert_eq!(cloned.loaded, stats.loaded);
644 assert_eq!(cloned.discovered, stats.discovered);
645 assert_eq!(cloned.start_time, stats.start_time);
646 }
647
648 #[test]
651 fn test_plugin_discovery_success() {
652 let plugin_id = PluginId("test-plugin".to_string());
653 let manifest = PluginManifest::new(PluginInfo::new(
654 plugin_id.clone(),
655 PluginVersion::new(1, 0, 0),
656 "Test Plugin",
657 "A test plugin",
658 PluginAuthor::new("test-author"),
659 ));
660
661 let result = PluginDiscovery::success(plugin_id, manifest, "/path/to/plugin".to_string());
662
663 assert!(result.is_success());
664 assert!(result.first_error().is_none());
665 assert!(result.is_valid);
666 assert!(result.errors.is_empty());
667 }
668
669 #[test]
670 fn test_plugin_discovery_failure() {
671 let plugin_id = PluginId("failing-plugin".to_string());
672 let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
673
674 let result =
675 PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors.clone());
676
677 assert!(!result.is_success());
678 assert_eq!(result.first_error(), Some("Error 1"));
679 assert_eq!(result.errors.len(), 2);
680 assert!(!result.is_valid);
681 }
682
683 #[test]
684 fn test_plugin_discovery_failure_with_empty_errors() {
685 let plugin_id = PluginId("failing-plugin".to_string());
686 let errors = vec![];
687
688 let result = PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors);
689
690 assert!(!result.is_success());
691 assert!(result.first_error().is_none());
692 assert!(result.errors.is_empty());
693 }
694
695 #[test]
696 fn test_plugin_discovery_clone() {
697 let plugin_id = PluginId("test-plugin".to_string());
698 let manifest = PluginManifest::new(PluginInfo::new(
699 plugin_id.clone(),
700 PluginVersion::new(1, 0, 0),
701 "Test",
702 "Test",
703 PluginAuthor::new("test"),
704 ));
705
706 let discovery = PluginDiscovery::success(plugin_id, manifest, "/path".to_string());
707 let cloned = discovery.clone();
708
709 assert_eq!(discovery.plugin_id, cloned.plugin_id);
710 assert_eq!(discovery.is_valid, cloned.is_valid);
711 }
712
713 #[test]
716 fn test_module_exports() {
717 let _ = std::marker::PhantomData::<PluginLoader>;
719 let _ = std::marker::PhantomData::<PluginRegistry>;
720 let _ = std::marker::PhantomData::<PluginValidator>;
721 }
723
724 #[test]
725 fn test_loader_result_type() {
726 let success: LoaderResult<i32> = Ok(42);
727 assert!(success.is_ok());
728 match success {
729 Ok(val) => assert_eq!(val, 42),
730 Err(_) => panic!("expected Ok"),
731 }
732
733 let error: LoaderResult<i32> = Err(PluginLoaderError::load("test"));
734 assert!(error.is_err());
735 }
736}