Skip to main content

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// Type aliases for complex plugin function signatures
9type RenderFunction = fn(&PluginContext, &ComponentConfig) -> Result<String, PluginError>;
10type UpdateFunction = fn(&PluginContext, &ComponentConfig) -> Result<ComponentState, PluginError>;
11type EventHandler = fn(&PluginContext, &PluginEvent) -> Result<(), PluginError>;
12
13// Type aliases for Symbol<> function signatures
14type RenderSymbol<'a> = Symbol<'a, RenderFunction>;
15type UpdateSymbol<'a> = Symbol<'a, UpdateFunction>;
16type EventSymbol<'a> = Symbol<'a, EventHandler>;
17
18/// Plugin manifest structure
19#[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/// Plugin dependency specification
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PluginDependency {
34    pub name: String,
35    pub version: String,
36    pub required: bool,
37}
38
39/// Plugin permission types
40#[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
48/// Plugin component definition
49pub 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/// Plugin execution context
57#[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/// Component configuration from YAML
66#[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/// Component state for updates
75#[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/// Plugin events
83#[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/// Plugin errors
104#[derive(Debug, Clone)]
105pub enum PluginError {
106    InitializationFailed(String),
107    RenderFailed(String),
108    PermissionDenied(String),
109    InvalidConfiguration(String),
110    RuntimeError(String),
111}
112
113/// Plugin registry for managing loaded plugins
114#[derive(Debug)]
115pub struct PluginRegistry {
116    plugins: HashMap<String, LoadedPlugin>,
117    component_types: HashMap<String, String>, // component_type -> plugin_name
118    security_manager: PluginSecurityManager,
119}
120
121/// Loaded plugin instance
122struct 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/// Security manager for plugin permissions
146#[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    /// Load a plugin from a directory
170    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        // Validate permissions
175        self.security_manager
176            .validate_permissions(&manifest.permissions)?;
177
178        // Try to load dynamic library first, fall back to mock if not available
179        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            // Fall back to mock implementation for testing/development
184            (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        // Register component types
196        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    /// Get a component by type
206    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    /// Render a plugin component
216    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    /// Handle plugin events
233    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(()) // No event handler defined
244            }
245        } else {
246            Err(PluginError::InvalidConfiguration(format!(
247                "Component type '{}' not found",
248                component_type
249            )))
250        }
251    }
252
253    /// List loaded plugins
254    pub fn list_plugins(&self) -> Vec<&PluginManifest> {
255        self.plugins.values().map(|p| &p.manifest).collect()
256    }
257
258    /// Unload a plugin
259    pub fn unload_plugin(&mut self, plugin_name: &str) -> Result<(), PluginError> {
260        if let Some(plugin) = self.plugins.remove(plugin_name) {
261            // Remove component type registrations
262            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        // Try to read actual TOML file, fall back to mock for testing
279        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            // Return mock manifest for testing when no actual file exists
289            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    /// Load dynamic library and extract components
303    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 each component type, try to load the required functions
316            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                // Load render function (required)
322                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                // Load update function (optional)
331                let update_fn = library
332                    .get(update_fn_name.as_bytes())
333                    .ok()
334                    .map(|symbol: UpdateSymbol| *symbol.into_raw());
335
336                // Load event handler (optional)
337                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    /// Load mock components for testing/fallback
357    fn load_mock_components(
358        &self,
359        manifest: &PluginManifest,
360    ) -> Result<HashMap<String, PluginComponent>, PluginError> {
361        let mut components = HashMap::new();
362
363        // Mock component loading for testing when no dynamic library available
364        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                    // Environment variable access is generally allowed
423                }
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
444// Mock functions for testing - would be replaced by actual plugin code
445fn 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}