1use crate::model::common::Bounds;
2use crate::AppContext;
3use libloading::{Library, Symbol};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PluginManifest {
11 pub name: String,
12 pub version: String,
13 pub author: String,
14 pub description: String,
15 pub entry_point: String,
16 pub component_types: Vec<String>,
17 pub dependencies: Vec<PluginDependency>,
18 pub permissions: Vec<PluginPermission>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct PluginDependency {
24 pub name: String,
25 pub version: String,
26 pub required: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum PluginPermission {
32 FileSystem { paths: Vec<String> },
33 Network { hosts: Vec<String> },
34 Process { commands: Vec<String> },
35 Environment { variables: Vec<String> },
36}
37
38pub struct PluginComponent {
40 pub component_type: String,
41 pub render_fn: fn(&PluginContext, &ComponentConfig) -> Result<String, PluginError>,
42 pub update_fn:
43 Option<fn(&PluginContext, &ComponentConfig) -> Result<ComponentState, PluginError>>,
44 pub event_handler: Option<fn(&PluginContext, &PluginEvent) -> Result<(), PluginError>>,
45}
46
47#[derive(Debug, Clone)]
49pub struct PluginContext {
50 pub app_context: AppContext,
51 pub muxbox_bounds: Bounds,
52 pub plugin_data: HashMap<String, serde_json::Value>,
53 pub permissions: Vec<PluginPermission>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ComponentConfig {
59 pub component_type: String,
60 pub properties: HashMap<String, serde_json::Value>,
61 pub data_source: Option<String>,
62 pub refresh_interval: Option<u64>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ComponentState {
68 pub content: String,
69 pub metadata: HashMap<String, serde_json::Value>,
70 pub needs_refresh: bool,
71}
72
73#[derive(Debug, Clone)]
75pub enum PluginEvent {
76 KeyPress(String),
77 MouseEvent {
78 x: u16,
79 y: u16,
80 action: String,
81 },
82 Timer {
83 interval: u64,
84 },
85 DataUpdate {
86 source: String,
87 data: serde_json::Value,
88 },
89 MuxBoxResize {
90 new_bounds: Bounds,
91 },
92}
93
94#[derive(Debug, Clone)]
96pub enum PluginError {
97 InitializationFailed(String),
98 RenderFailed(String),
99 PermissionDenied(String),
100 InvalidConfiguration(String),
101 RuntimeError(String),
102}
103
104#[derive(Debug)]
106pub struct PluginRegistry {
107 plugins: HashMap<String, LoadedPlugin>,
108 component_types: HashMap<String, String>, security_manager: PluginSecurityManager,
110}
111
112struct LoadedPlugin {
114 manifest: PluginManifest,
115 components: HashMap<String, PluginComponent>,
116 library: Option<Library>,
117 is_active: bool,
118 load_time: std::time::SystemTime,
119}
120
121impl std::fmt::Debug for LoadedPlugin {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("LoadedPlugin")
124 .field("manifest", &self.manifest)
125 .field(
126 "components",
127 &format!("{} components", self.components.len()),
128 )
129 .field("library_loaded", &self.library.is_some())
130 .field("is_active", &self.is_active)
131 .field("load_time", &self.load_time)
132 .finish()
133 }
134}
135
136#[derive(Debug)]
138struct PluginSecurityManager {
139 allowed_paths: Vec<String>,
140 allowed_hosts: Vec<String>,
141 allowed_commands: Vec<String>,
142 sandbox_enabled: bool,
143}
144
145impl PluginRegistry {
146 pub fn new() -> Self {
147 Self {
148 plugins: HashMap::new(),
149 component_types: HashMap::new(),
150 security_manager: PluginSecurityManager::new(),
151 }
152 }
153
154 pub fn load_plugin<P: AsRef<Path>>(&mut self, plugin_path: P) -> Result<(), PluginError> {
156 let manifest_path = plugin_path.as_ref().join("plugin.toml");
157 let manifest = self.load_manifest(&manifest_path)?;
158
159 self.security_manager
161 .validate_permissions(&manifest.permissions)?;
162
163 let library_path = plugin_path.as_ref().join(&manifest.entry_point);
165 let (library, components) = if library_path.exists() {
166 self.load_dynamic_library(&library_path, &manifest)?
167 } else {
168 (None, self.load_mock_components(&manifest)?)
170 };
171
172 let loaded_plugin = LoadedPlugin {
173 manifest: manifest.clone(),
174 components,
175 library,
176 is_active: true,
177 load_time: std::time::SystemTime::now(),
178 };
179
180 for component_type in &manifest.component_types {
182 self.component_types
183 .insert(component_type.clone(), manifest.name.clone());
184 }
185
186 self.plugins.insert(manifest.name.clone(), loaded_plugin);
187 Ok(())
188 }
189
190 pub fn get_component(&self, component_type: &str) -> Option<&PluginComponent> {
192 if let Some(plugin_name) = self.component_types.get(component_type) {
193 if let Some(plugin) = self.plugins.get(plugin_name) {
194 return plugin.components.get(component_type);
195 }
196 }
197 None
198 }
199
200 pub fn render_component(
202 &self,
203 component_type: &str,
204 context: &PluginContext,
205 config: &ComponentConfig,
206 ) -> Result<String, PluginError> {
207 if let Some(component) = self.get_component(component_type) {
208 (component.render_fn)(context, config)
209 } else {
210 Err(PluginError::InvalidConfiguration(format!(
211 "Component type '{}' not found",
212 component_type
213 )))
214 }
215 }
216
217 pub fn handle_event(
219 &self,
220 component_type: &str,
221 context: &PluginContext,
222 event: &PluginEvent,
223 ) -> Result<(), PluginError> {
224 if let Some(component) = self.get_component(component_type) {
225 if let Some(handler) = &component.event_handler {
226 handler(context, event)
227 } else {
228 Ok(()) }
230 } else {
231 Err(PluginError::InvalidConfiguration(format!(
232 "Component type '{}' not found",
233 component_type
234 )))
235 }
236 }
237
238 pub fn list_plugins(&self) -> Vec<&PluginManifest> {
240 self.plugins.values().map(|p| &p.manifest).collect()
241 }
242
243 pub fn unload_plugin(&mut self, plugin_name: &str) -> Result<(), PluginError> {
245 if let Some(plugin) = self.plugins.remove(plugin_name) {
246 for component_type in &plugin.manifest.component_types {
248 self.component_types.remove(component_type);
249 }
250 Ok(())
251 } else {
252 Err(PluginError::InvalidConfiguration(format!(
253 "Plugin '{}' not found",
254 plugin_name
255 )))
256 }
257 }
258
259 pub fn load_manifest<P: AsRef<Path>>(
260 &self,
261 manifest_path: P,
262 ) -> Result<PluginManifest, PluginError> {
263 if manifest_path.as_ref().exists() {
265 let content = std::fs::read_to_string(manifest_path).map_err(|e| {
266 PluginError::InitializationFailed(format!("Failed to read manifest: {}", e))
267 })?;
268
269 toml::from_str(&content).map_err(|e| {
270 PluginError::InitializationFailed(format!("Failed to parse manifest: {}", e))
271 })
272 } else {
273 Ok(PluginManifest {
275 name: "test_plugin".to_string(),
276 version: "1.0.0".to_string(),
277 author: "BoxMux Team".to_string(),
278 description: "Test plugin".to_string(),
279 entry_point: "lib.so".to_string(),
280 component_types: vec!["custom_chart".to_string()],
281 dependencies: vec![],
282 permissions: vec![],
283 })
284 }
285 }
286
287 fn load_dynamic_library<P: AsRef<Path>>(
289 &self,
290 library_path: P,
291 manifest: &PluginManifest,
292 ) -> Result<(Option<Library>, HashMap<String, PluginComponent>), PluginError> {
293 unsafe {
294 let library = Library::new(library_path.as_ref()).map_err(|e| {
295 PluginError::InitializationFailed(format!("Failed to load library: {}", e))
296 })?;
297
298 let mut components = HashMap::new();
299
300 for component_type in &manifest.component_types {
302 let render_fn_name = format!("{}_render", component_type);
303 let update_fn_name = format!("{}_update", component_type);
304 let event_fn_name = format!("{}_event", component_type);
305
306 let render_symbol: Symbol<
308 fn(&PluginContext, &ComponentConfig) -> Result<String, PluginError>,
309 > = library.get(render_fn_name.as_bytes()).map_err(|e| {
310 PluginError::InitializationFailed(format!(
311 "Failed to load render function '{}': {}",
312 render_fn_name, e
313 ))
314 })?;
315
316 let update_fn = library.get(update_fn_name.as_bytes()).ok().map(
318 |symbol: Symbol<
319 fn(&PluginContext, &ComponentConfig) -> Result<ComponentState, PluginError>,
320 >| { *symbol.into_raw() },
321 );
322
323 let event_handler = library.get(event_fn_name.as_bytes()).ok().map(
325 |symbol: Symbol<
326 fn(&PluginContext, &PluginEvent) -> Result<(), PluginError>,
327 >| { *symbol.into_raw() },
328 );
329
330 let component = PluginComponent {
331 component_type: component_type.clone(),
332 render_fn: *render_symbol.into_raw(),
333 update_fn,
334 event_handler,
335 };
336
337 components.insert(component_type.clone(), component);
338 }
339
340 Ok((Some(library), components))
341 }
342 }
343
344 fn load_mock_components(
346 &self,
347 manifest: &PluginManifest,
348 ) -> Result<HashMap<String, PluginComponent>, PluginError> {
349 let mut components = HashMap::new();
350
351 for component_type in &manifest.component_types {
353 let component = PluginComponent {
354 component_type: component_type.clone(),
355 render_fn: mock_render_function,
356 update_fn: Some(mock_update_function),
357 event_handler: Some(mock_event_handler),
358 };
359 components.insert(component_type.clone(), component);
360 }
361
362 Ok(components)
363 }
364}
365
366impl PluginSecurityManager {
367 fn new() -> Self {
368 Self {
369 allowed_paths: vec!["/tmp".to_string(), "/var/log".to_string()],
370 allowed_hosts: vec!["localhost".to_string()],
371 allowed_commands: vec!["echo".to_string(), "date".to_string()],
372 sandbox_enabled: true,
373 }
374 }
375
376 fn validate_permissions(&self, permissions: &[PluginPermission]) -> Result<(), PluginError> {
377 for permission in permissions {
378 match permission {
379 PluginPermission::FileSystem { paths } => {
380 for path in paths {
381 if !self.is_path_allowed(path) {
382 return Err(PluginError::PermissionDenied(format!(
383 "File system access to '{}' not allowed",
384 path
385 )));
386 }
387 }
388 }
389 PluginPermission::Network { hosts } => {
390 for host in hosts {
391 if !self.is_host_allowed(host) {
392 return Err(PluginError::PermissionDenied(format!(
393 "Network access to '{}' not allowed",
394 host
395 )));
396 }
397 }
398 }
399 PluginPermission::Process { commands } => {
400 for command in commands {
401 if !self.is_command_allowed(command) {
402 return Err(PluginError::PermissionDenied(format!(
403 "Process execution of '{}' not allowed",
404 command
405 )));
406 }
407 }
408 }
409 PluginPermission::Environment { variables: _ } => {
410 }
412 }
413 }
414 Ok(())
415 }
416
417 fn is_path_allowed(&self, path: &str) -> bool {
418 self.allowed_paths
419 .iter()
420 .any(|allowed| path.starts_with(allowed))
421 }
422
423 fn is_host_allowed(&self, host: &str) -> bool {
424 self.allowed_hosts.contains(&host.to_string())
425 }
426
427 fn is_command_allowed(&self, command: &str) -> bool {
428 self.allowed_commands.contains(&command.to_string())
429 }
430}
431
432fn mock_render_function(
434 _context: &PluginContext,
435 config: &ComponentConfig,
436) -> Result<String, PluginError> {
437 Ok(format!("Custom component: {}", config.component_type))
438}
439
440fn mock_update_function(
441 _context: &PluginContext,
442 _config: &ComponentConfig,
443) -> Result<ComponentState, PluginError> {
444 Ok(ComponentState {
445 content: "Updated content".to_string(),
446 metadata: HashMap::new(),
447 needs_refresh: false,
448 })
449}
450
451fn mock_event_handler(_context: &PluginContext, event: &PluginEvent) -> Result<(), PluginError> {
452 match event {
453 PluginEvent::KeyPress(key) => {
454 println!("Plugin received key press: {}", key);
455 }
456 _ => {}
457 }
458 Ok(())
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_plugin_registry_creation() {
467 let registry = PluginRegistry::new();
468 assert_eq!(registry.plugins.len(), 0);
469 assert_eq!(registry.component_types.len(), 0);
470 }
471
472 #[test]
473 fn test_plugin_manifest_serialization() {
474 let manifest = PluginManifest {
475 name: "test".to_string(),
476 version: "1.0.0".to_string(),
477 author: "test_author".to_string(),
478 description: "Test plugin".to_string(),
479 entry_point: "lib.so".to_string(),
480 component_types: vec!["custom_type".to_string()],
481 dependencies: vec![],
482 permissions: vec![PluginPermission::FileSystem {
483 paths: vec!["/tmp".to_string()],
484 }],
485 };
486
487 let serialized = serde_json::to_string(&manifest).unwrap();
488 let deserialized: PluginManifest = serde_json::from_str(&serialized).unwrap();
489
490 assert_eq!(manifest.name, deserialized.name);
491 assert_eq!(manifest.version, deserialized.version);
492 }
493
494 #[test]
495 fn test_security_manager_path_validation() {
496 let security_manager = PluginSecurityManager::new();
497
498 assert!(security_manager.is_path_allowed("/tmp/test"));
499 assert!(!security_manager.is_path_allowed("/etc/passwd"));
500 }
501
502 #[test]
503 fn test_component_config_parsing() {
504 let config_json = r#"{
505 "component_type": "custom_chart",
506 "properties": {
507 "title": "Test Chart",
508 "data_source": "metrics"
509 },
510 "refresh_interval": 5000
511 }"#;
512
513 let config: ComponentConfig = serde_json::from_str(config_json).unwrap();
514 assert_eq!(config.component_type, "custom_chart");
515 assert_eq!(config.refresh_interval, Some(5000));
516 }
517
518 #[test]
519 fn test_plugin_error_display() {
520 let error = PluginError::PermissionDenied("Test error".to_string());
521 assert!(format!("{:?}", error).contains("Test error"));
522 }
523}