boxmux_lib/
plugin.rs

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/// Plugin manifest structure
9#[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/// Plugin dependency specification
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct PluginDependency {
24    pub name: String,
25    pub version: String,
26    pub required: bool,
27}
28
29/// Plugin permission types
30#[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
38/// Plugin component definition
39pub 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/// Plugin execution context
48#[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/// Component configuration from YAML
57#[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/// Component state for updates
66#[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/// Plugin events
74#[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/// Plugin errors
95#[derive(Debug, Clone)]
96pub enum PluginError {
97    InitializationFailed(String),
98    RenderFailed(String),
99    PermissionDenied(String),
100    InvalidConfiguration(String),
101    RuntimeError(String),
102}
103
104/// Plugin registry for managing loaded plugins
105#[derive(Debug)]
106pub struct PluginRegistry {
107    plugins: HashMap<String, LoadedPlugin>,
108    component_types: HashMap<String, String>, // component_type -> plugin_name
109    security_manager: PluginSecurityManager,
110}
111
112/// Loaded plugin instance
113struct 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/// Security manager for plugin permissions
137#[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    /// Load a plugin from a directory
155    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        // Validate permissions
160        self.security_manager
161            .validate_permissions(&manifest.permissions)?;
162
163        // Try to load dynamic library first, fall back to mock if not available
164        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            // Fall back to mock implementation for testing/development
169            (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        // Register component types
181        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    /// Get a component by type
191    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    /// Render a plugin component
201    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    /// Handle plugin events
218    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(()) // No event handler defined
229            }
230        } else {
231            Err(PluginError::InvalidConfiguration(format!(
232                "Component type '{}' not found",
233                component_type
234            )))
235        }
236    }
237
238    /// List loaded plugins
239    pub fn list_plugins(&self) -> Vec<&PluginManifest> {
240        self.plugins.values().map(|p| &p.manifest).collect()
241    }
242
243    /// Unload a plugin
244    pub fn unload_plugin(&mut self, plugin_name: &str) -> Result<(), PluginError> {
245        if let Some(plugin) = self.plugins.remove(plugin_name) {
246            // Remove component type registrations
247            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        // Try to read actual TOML file, fall back to mock for testing
264        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            // Return mock manifest for testing when no actual file exists
274            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    /// Load dynamic library and extract components
288    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 each component type, try to load the required functions
301            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                // Load render function (required)
307                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                // Load update function (optional)
317                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                // Load event handler (optional)
324                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    /// Load mock components for testing/fallback
345    fn load_mock_components(
346        &self,
347        manifest: &PluginManifest,
348    ) -> Result<HashMap<String, PluginComponent>, PluginError> {
349        let mut components = HashMap::new();
350
351        // Mock component loading for testing when no dynamic library available
352        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                    // Environment variable access is generally allowed
411                }
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
432// Mock functions for testing - would be replaced by actual plugin code
433fn 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}