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