Skip to main content

pdf_ast/plugins/
loader.rs

1#![allow(clippy::items_after_test_module)]
2
3use super::*;
4use crate::plugins::registry::PluginRegistry;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9/// Plugin loader for loading plugins from various sources
10pub struct PluginLoader {
11    registry: Arc<PluginRegistry>,
12    search_paths: Vec<PathBuf>,
13    loaded_plugins: std::collections::HashMap<String, LoadedPlugin>,
14}
15
16/// Information about a loaded plugin
17#[derive(Clone)]
18pub struct LoadedPlugin {
19    pub name: String,
20    pub path: Option<PathBuf>,
21    pub metadata: PluginMetadata,
22    pub load_time: chrono::DateTime<chrono::Utc>,
23    #[allow(dead_code)]
24    library: Option<std::sync::Arc<LibraryHandle>>,
25}
26
27/// Plugin loading configuration
28#[derive(Debug, Clone)]
29pub struct LoadConfig {
30    pub auto_resolve_dependencies: bool,
31    pub allow_version_conflicts: bool,
32    pub validate_signatures: bool,
33    pub sandbox_mode: bool,
34}
35
36impl Default for LoadConfig {
37    fn default() -> Self {
38        Self {
39            auto_resolve_dependencies: true,
40            allow_version_conflicts: false,
41            validate_signatures: false,
42            sandbox_mode: true,
43        }
44    }
45}
46
47impl PluginLoader {
48    pub fn new(registry: Arc<PluginRegistry>) -> Self {
49        Self {
50            registry,
51            search_paths: Vec::new(),
52            loaded_plugins: std::collections::HashMap::new(),
53        }
54    }
55
56    /// Add a search path for plugins
57    pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) {
58        self.search_paths.push(path.as_ref().to_path_buf());
59    }
60
61    /// Load plugin from memory (for built-in plugins)
62    pub fn load_plugin(&mut self, plugin: Arc<dyn AstPlugin>) -> PluginResult {
63        let metadata = plugin.metadata().clone();
64        let name = metadata.name.clone();
65
66        // Register with registry
67        match self.registry.register(plugin) {
68            PluginResult::Success => {
69                // Track loaded plugin
70                let loaded_plugin = LoadedPlugin {
71                    name: name.clone(),
72                    path: None,
73                    metadata,
74                    load_time: chrono::Utc::now(),
75                    library: None,
76                };
77                self.loaded_plugins.insert(name, loaded_plugin);
78                PluginResult::Success
79            }
80            result => result,
81        }
82    }
83
84    /// Load plugin from configuration
85    pub fn load_from_config(
86        &mut self,
87        config: &serde_json::Value,
88        load_config: &LoadConfig,
89    ) -> PluginResult {
90        // Parse plugin configuration
91        let plugin_name = match config.get("name").and_then(|v| v.as_str()) {
92            Some(name) => name,
93            None => return PluginResult::Error("Missing plugin name in config".to_string()),
94        };
95
96        let plugin_type = config
97            .get("type")
98            .and_then(|v| v.as_str())
99            .unwrap_or("dynamic");
100
101        match plugin_type {
102            "builtin" => self.load_builtin_plugin(plugin_name, config, load_config),
103            "dynamic" => self.load_dynamic_plugin(plugin_name, config, load_config),
104            _ => PluginResult::Error(format!("Unknown plugin type: {}", plugin_type)),
105        }
106    }
107
108    /// Load multiple plugins with dependency resolution
109    pub fn load_plugins(
110        &mut self,
111        configs: &[serde_json::Value],
112        load_config: &LoadConfig,
113    ) -> Vec<PluginResult> {
114        let mut results = Vec::new();
115
116        if load_config.auto_resolve_dependencies {
117            // Extract plugin names and resolve dependencies
118            let plugin_names: Vec<String> = configs
119                .iter()
120                .filter_map(|config| {
121                    config
122                        .get("name")
123                        .and_then(|v| v.as_str())
124                        .map(|s| s.to_string())
125                })
126                .collect();
127
128            match self.registry.get_dependency_order(&plugin_names) {
129                Ok(ordered_names) => {
130                    // Load plugins in dependency order
131                    for name in ordered_names {
132                        if let Some(config) = configs.iter().find(|c| {
133                            c.get("name")
134                                .and_then(|v| v.as_str())
135                                .map(|s| s == name)
136                                .unwrap_or(false)
137                        }) {
138                            let result = self.load_from_config(config, load_config);
139                            results.push(result);
140                        }
141                    }
142                }
143                Err(err) => {
144                    results.push(PluginResult::Error(err));
145                }
146            }
147        } else {
148            // Load plugins in order provided
149            for config in configs {
150                let result = self.load_from_config(config, load_config);
151                results.push(result);
152            }
153        }
154
155        results
156    }
157
158    /// Discover plugins in search paths
159    pub fn discover_plugins(&self) -> Vec<PathBuf> {
160        let mut discovered = Vec::new();
161
162        for search_path in &self.search_paths {
163            if let Ok(entries) = fs::read_dir(search_path) {
164                for entry in entries.flatten() {
165                    let path = entry.path();
166
167                    // Look for plugin manifests or libraries
168                    if path.extension().and_then(|s| s.to_str()) == Some("json") {
169                        // Plugin manifest
170                        discovered.push(path);
171                    } else if path.extension().and_then(|s| s.to_str()) == Some("so")
172                        || path.extension().and_then(|s| s.to_str()) == Some("dll")
173                        || path.extension().and_then(|s| s.to_str()) == Some("dylib")
174                    {
175                        // Dynamic library
176                        discovered.push(path);
177                    }
178                }
179            }
180        }
181
182        discovered
183    }
184
185    /// Unload a plugin
186    pub fn unload_plugin(&mut self, name: &str) -> PluginResult {
187        // Remove from registry
188        let registry_result = self.registry.unregister(name);
189
190        // Remove from loaded plugins tracking
191        self.loaded_plugins.remove(name);
192
193        registry_result
194    }
195
196    /// Get information about loaded plugins
197    pub fn get_loaded_plugins(&self) -> Vec<LoadedPlugin> {
198        self.loaded_plugins.values().cloned().collect()
199    }
200
201    /// Check if a plugin is loaded
202    pub fn is_loaded(&self, name: &str) -> bool {
203        self.loaded_plugins.contains_key(name)
204    }
205
206    /// Reload a plugin
207    pub fn reload_plugin(&mut self, name: &str, load_config: &LoadConfig) -> PluginResult {
208        // Get current plugin info
209        let loaded_plugin = self.loaded_plugins.get(name).cloned();
210
211        if let Some(plugin_info) = loaded_plugin {
212            // Unload current plugin
213            let unload_result = self.unload_plugin(name);
214            if let PluginResult::Error(msg) = unload_result {
215                return PluginResult::Error(msg);
216            }
217
218            // Try to reload from original path or config
219            if let Some(path) = plugin_info.path {
220                // Reload from file
221                self.load_from_path(&path, load_config)
222            } else {
223                PluginResult::Error("Cannot reload in-memory plugin without source".to_string())
224            }
225        } else {
226            PluginResult::Error(format!("Plugin '{}' is not loaded", name))
227        }
228    }
229
230    /// Load plugin from file path
231    pub fn load_from_path(&mut self, path: &Path, load_config: &LoadConfig) -> PluginResult {
232        match path.extension().and_then(|s| s.to_str()) {
233            Some("json") => self.load_from_manifest(path, load_config),
234            Some("so") | Some("dll") | Some("dylib") => {
235                self.load_dynamic_library(path, load_config)
236            }
237            _ => PluginResult::Error(format!("Unsupported plugin file: {:?}", path)),
238        }
239    }
240
241    /// Load plugin from manifest file
242    fn load_from_manifest(&mut self, path: &Path, load_config: &LoadConfig) -> PluginResult {
243        match fs::read_to_string(path) {
244            Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
245                Ok(config) => self.load_from_config(&config, load_config),
246                Err(err) => PluginResult::Error(format!("Invalid plugin manifest: {}", err)),
247            },
248            Err(err) => PluginResult::Error(format!("Failed to read manifest: {}", err)),
249        }
250    }
251
252    /// Load built-in plugin
253    fn load_builtin_plugin(
254        &mut self,
255        name: &str,
256        config: &serde_json::Value,
257        _load_config: &LoadConfig,
258    ) -> PluginResult {
259        // Create built-in plugin instance based on name
260        let plugin: Arc<dyn AstPlugin> = match name {
261            "basic_validator" => Arc::new(BasicValidatorPlugin::new()),
262            "basic_transformer" => Arc::new(BasicTransformerPlugin::new()),
263            "structure_analyzer" => Arc::new(StructureAnalyzerPlugin::new()),
264            "security_scanner" => Arc::new(SecurityScannerPlugin::new()),
265            "metadata_extractor" => Arc::new(MetadataExtractorPlugin::new()),
266            _ => return PluginResult::Error(format!("Unknown built-in plugin: {}", name)),
267        };
268
269        // Apply configuration if provided
270        if let Some(params) = config.get("parameters") {
271            // Store parameters in plugin context for runtime use
272            let _ = params;
273        }
274
275        self.load_plugin(plugin)
276    }
277
278    /// Load dynamic plugin from library
279    fn load_dynamic_plugin(
280        &mut self,
281        name: &str,
282        config: &serde_json::Value,
283        load_config: &LoadConfig,
284    ) -> PluginResult {
285        // Get library path from config
286        let lib_path = match config.get("path").and_then(|v| v.as_str()) {
287            Some(path) => PathBuf::from(path),
288            None => {
289                // Search for library in search paths
290                if let Some(path) = self.find_plugin_library(name) {
291                    path
292                } else {
293                    return PluginResult::Error(format!(
294                        "Cannot find library for plugin: {}",
295                        name
296                    ));
297                }
298            }
299        };
300
301        self.load_dynamic_library(&lib_path, load_config)
302    }
303
304    /// Load dynamic library
305    fn load_dynamic_library(&mut self, path: &Path, load_config: &LoadConfig) -> PluginResult {
306        // Verify library exists
307        if !path.exists() {
308            return PluginResult::Error(format!("Library not found: {:?}", path));
309        }
310
311        // Validate library signature if required
312        if load_config.validate_signatures && !self.validate_library_signature(path) {
313            return PluginResult::Error(format!("Invalid library signature: {:?}", path));
314        }
315
316        // Load library using platform-specific loader
317        match DynamicLibraryLoader::load(path, load_config.sandbox_mode) {
318            Ok(library) => {
319                let library_handle = library.handle();
320                // Get plugin factory function
321                match library.get_plugin_factory() {
322                    Ok(plugin) => {
323                        // Use plugin instance
324                        let metadata = plugin.metadata().clone();
325                        let name = metadata.name.clone();
326
327                        // Store library handle
328                        let loaded_plugin = LoadedPlugin {
329                            name: name.clone(),
330                            path: Some(path.to_path_buf()),
331                            metadata,
332                            load_time: chrono::Utc::now(),
333                            library: Some(library_handle),
334                        };
335
336                        // Register plugin
337                        self.loaded_plugins.insert(name, loaded_plugin);
338                        self.load_plugin(plugin)
339                    }
340                    Err(err) => {
341                        PluginResult::Error(format!("Failed to get plugin factory: {}", err))
342                    }
343                }
344            }
345            Err(err) => PluginResult::Error(format!("Failed to load library: {}", err)),
346        }
347    }
348
349    /// Find plugin library in search paths
350    fn find_plugin_library(&self, name: &str) -> Option<PathBuf> {
351        let lib_extensions = if cfg!(target_os = "windows") {
352            vec!["dll"]
353        } else if cfg!(target_os = "macos") {
354            vec!["dylib"]
355        } else {
356            vec!["so"]
357        };
358
359        for search_path in &self.search_paths {
360            for ext in &lib_extensions {
361                let lib_name = format!("lib{}.{}", name, ext);
362                let lib_path = search_path.join(&lib_name);
363                if lib_path.exists() {
364                    return Some(lib_path);
365                }
366
367                // Also try without "lib" prefix
368                let lib_name = format!("{}.{}", name, ext);
369                let lib_path = search_path.join(&lib_name);
370                if lib_path.exists() {
371                    return Some(lib_path);
372                }
373            }
374        }
375
376        None
377    }
378
379    /// Validate library signature
380    fn validate_library_signature(&self, path: &Path) -> bool {
381        // In production, would verify cryptographic signature
382        // For now, just check if file is readable
383        path.exists() && path.is_file()
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_load_missing_dynamic_library() {
393        let registry = Arc::new(PluginRegistry::new());
394        let mut loader = PluginLoader::new(registry);
395        let load_config = LoadConfig::default();
396        let path = Path::new("nonexistent_plugin.so");
397        let result = loader.load_from_path(path, &load_config);
398        assert!(matches!(result, PluginResult::Error(_)));
399    }
400}
401
402/// Built-in plugin factories
403pub struct BuiltinPlugins;
404
405impl BuiltinPlugins {
406    /// Create a simple validation plugin
407    pub fn create_basic_validator() -> Box<dyn AstPlugin> {
408        Box::new(BasicValidatorPlugin::new())
409    }
410
411    /// Create a simple transformer plugin
412    pub fn create_basic_transformer() -> Box<dyn AstPlugin> {
413        Box::new(BasicTransformerPlugin::new())
414    }
415
416    /// Get list of available built-in plugins
417    pub fn list_available() -> Vec<String> {
418        vec![
419            "basic_validator".to_string(),
420            "basic_transformer".to_string(),
421        ]
422    }
423}
424
425/// Example basic validator plugin
426pub struct BasicValidatorPlugin {
427    metadata: PluginMetadata,
428}
429
430impl Default for BasicValidatorPlugin {
431    fn default() -> Self {
432        Self::new()
433    }
434}
435
436impl BasicValidatorPlugin {
437    pub fn new() -> Self {
438        Self {
439            metadata: PluginMetadata::new(
440                "basic_validator",
441                "1.0.0",
442                "Basic document validation plugin",
443                "PDF-AST",
444            )
445            .with_tags(vec!["validation", "builtin"])
446            .with_supported_types(vec![NodeType::Catalog, NodeType::Pages]),
447        }
448    }
449}
450
451impl AstPlugin for BasicValidatorPlugin {
452    fn metadata(&self) -> &PluginMetadata {
453        &self.metadata
454    }
455
456    fn process_document(
457        &self,
458        document: &mut crate::ast::PdfDocument,
459        _context: &mut PluginContext,
460    ) -> PluginResult {
461        // Basic validation: check if document has catalog
462        if document.ast.get_root().is_none() {
463            PluginResult::Error("Document missing root catalog".to_string())
464        } else {
465            PluginResult::Success
466        }
467    }
468
469    fn clone_plugin(&self) -> Box<dyn AstPlugin> {
470        Box::new(Self::new())
471    }
472}
473
474/// Example basic transformer plugin
475pub struct BasicTransformerPlugin {
476    metadata: PluginMetadata,
477}
478
479impl Default for BasicTransformerPlugin {
480    fn default() -> Self {
481        Self::new()
482    }
483}
484
485impl BasicTransformerPlugin {
486    pub fn new() -> Self {
487        Self {
488            metadata: PluginMetadata::new(
489                "basic_transformer",
490                "1.0.0",
491                "Basic document transformation plugin",
492                "PDF-AST",
493            )
494            .with_tags(vec!["transformation", "builtin"]),
495        }
496    }
497}
498
499impl AstPlugin for BasicTransformerPlugin {
500    fn metadata(&self) -> &PluginMetadata {
501        &self.metadata
502    }
503
504    fn capabilities(&self) -> PluginCapabilities {
505        PluginCapabilities {
506            can_modify_nodes: true,
507            can_add_nodes: false,
508            can_remove_nodes: false,
509            can_validate: false,
510            can_transform: true,
511            requires_document_context: false,
512            thread_safe: true,
513        }
514    }
515
516    fn clone_plugin(&self) -> Box<dyn AstPlugin> {
517        Box::new(Self::new())
518    }
519}
520
521/// Structure analyzer plugin
522pub struct StructureAnalyzerPlugin {
523    metadata: PluginMetadata,
524}
525
526impl Default for StructureAnalyzerPlugin {
527    fn default() -> Self {
528        Self::new()
529    }
530}
531
532impl StructureAnalyzerPlugin {
533    pub fn new() -> Self {
534        Self {
535            metadata: PluginMetadata::new(
536                "structure_analyzer",
537                "1.0.0",
538                "Analyzes document structure and complexity",
539                "PDF-AST",
540            )
541            .with_tags(vec!["analysis", "builtin"]),
542        }
543    }
544}
545
546impl AstPlugin for StructureAnalyzerPlugin {
547    fn metadata(&self) -> &PluginMetadata {
548        &self.metadata
549    }
550
551    fn process_document(
552        &self,
553        document: &mut crate::ast::PdfDocument,
554        context: &mut PluginContext,
555    ) -> PluginResult {
556        // Analyze document structure
557        let node_count = document.ast.get_all_nodes().len();
558        let depth = document.ast.get_max_depth();
559
560        // Store analysis results in context
561        context.set_data("node_count", node_count.to_string());
562        context.set_data("max_depth", depth.to_string());
563
564        // Check for complexity issues
565        if node_count > 10000 {
566            context.add_warning("Document has high complexity (>10000 nodes)");
567        }
568
569        if depth > 50 {
570            context.add_warning("Document has deep nesting (>50 levels)");
571        }
572
573        PluginResult::Success
574    }
575
576    fn clone_plugin(&self) -> Box<dyn AstPlugin> {
577        Box::new(Self::new())
578    }
579}
580
581/// Security scanner plugin
582pub struct SecurityScannerPlugin {
583    metadata: PluginMetadata,
584}
585
586impl Default for SecurityScannerPlugin {
587    fn default() -> Self {
588        Self::new()
589    }
590}
591
592impl SecurityScannerPlugin {
593    pub fn new() -> Self {
594        Self {
595            metadata: PluginMetadata::new(
596                "security_scanner",
597                "1.0.0",
598                "Scans for security issues and suspicious patterns",
599                "PDF-AST",
600            )
601            .with_tags(vec!["security", "builtin"])
602            .with_supported_types(vec![
603                NodeType::JavaScriptAction,
604                NodeType::LaunchAction,
605                NodeType::URIAction,
606                NodeType::EmbeddedFile,
607            ]),
608        }
609    }
610}
611
612impl AstPlugin for SecurityScannerPlugin {
613    fn metadata(&self) -> &PluginMetadata {
614        &self.metadata
615    }
616
617    fn process_node(
618        &self,
619        node: &mut crate::ast::AstNode,
620        context: &mut PluginContext,
621    ) -> PluginResult {
622        match node.node_type {
623            NodeType::JavaScriptAction | NodeType::EmbeddedJS => {
624                context.add_warning(&format!("JavaScript detected at node {:?}", node.id));
625            }
626            NodeType::LaunchAction => {
627                context.add_warning(&format!("Launch action detected at node {:?}", node.id));
628            }
629            NodeType::URIAction => {
630                if let Some(uri) = node.metadata.get_property("URI") {
631                    if uri.starts_with("http://") {
632                        context.add_warning(&format!("Insecure HTTP URI at node {:?}", node.id));
633                    }
634                }
635            }
636            NodeType::EmbeddedFile => {
637                context.add_info(format!("Embedded file detected at node {:?}", node.id));
638            }
639            _ => {}
640        }
641
642        PluginResult::Success
643    }
644
645    fn clone_plugin(&self) -> Box<dyn AstPlugin> {
646        Box::new(Self::new())
647    }
648}
649
650/// Metadata extractor plugin
651pub struct MetadataExtractorPlugin {
652    metadata: PluginMetadata,
653}
654
655impl Default for MetadataExtractorPlugin {
656    fn default() -> Self {
657        Self::new()
658    }
659}
660
661impl MetadataExtractorPlugin {
662    pub fn new() -> Self {
663        Self {
664            metadata: PluginMetadata::new(
665                "metadata_extractor",
666                "1.0.0",
667                "Extracts and consolidates document metadata",
668                "PDF-AST",
669            )
670            .with_tags(vec!["metadata", "builtin"])
671            .with_supported_types(vec![NodeType::Metadata, NodeType::Catalog]),
672        }
673    }
674}
675
676impl AstPlugin for MetadataExtractorPlugin {
677    fn metadata(&self) -> &PluginMetadata {
678        &self.metadata
679    }
680
681    fn process_document(
682        &self,
683        document: &mut crate::ast::PdfDocument,
684        context: &mut PluginContext,
685    ) -> PluginResult {
686        // Extract document metadata
687        context.set_data("title", document.metadata.title.clone().unwrap_or_default());
688        context.set_data(
689            "author",
690            document.metadata.author.clone().unwrap_or_default(),
691        );
692        context.set_data(
693            "subject",
694            document.metadata.subject.clone().unwrap_or_default(),
695        );
696        context.set_data(
697            "creator",
698            document.metadata.creator.clone().unwrap_or_default(),
699        );
700        context.set_data(
701            "producer",
702            document.metadata.producer.clone().unwrap_or_default(),
703        );
704
705        if let Some(created) = &document.metadata.creation_date {
706            context.set_data("creation_date", created.to_string());
707        }
708
709        if let Some(modified) = &document.metadata.modification_date {
710            context.set_data("modification_date", modified.to_string());
711        }
712
713        context.set_data("page_count", document.metadata.page_count.to_string());
714        context.set_data("encrypted", document.metadata.encrypted.to_string());
715
716        PluginResult::Success
717    }
718
719    fn clone_plugin(&self) -> Box<dyn AstPlugin> {
720        Box::new(Self::new())
721    }
722}
723
724/// Dynamic library loader
725struct DynamicLibraryLoader {
726    handle: std::sync::Arc<LibraryHandle>,
727}
728
729impl DynamicLibraryLoader {
730    /// Load a dynamic library
731    fn load(path: &Path, sandbox: bool) -> Result<Self, String> {
732        // Platform-specific library loading
733        let handle = LibraryHandle::load(path, sandbox)?;
734        Ok(Self {
735            handle: std::sync::Arc::new(handle),
736        })
737    }
738
739    /// Get plugin factory function from library
740    fn get_plugin_factory(&self) -> Result<Arc<dyn AstPlugin>, String> {
741        self.handle.get_plugin_factory()
742    }
743
744    fn handle(&self) -> std::sync::Arc<LibraryHandle> {
745        std::sync::Arc::clone(&self.handle)
746    }
747}
748
749/// Platform-specific library handle
750#[allow(dead_code)]
751enum LibraryHandle {
752    #[cfg(unix)]
753    Unix(UnixLibrary),
754    #[cfg(windows)]
755    Windows(WindowsLibrary),
756    Stub,
757}
758
759// Safety: library handles are OS resources that can be shared across threads;
760// access is synchronized by callers when invoking plugin factories.
761unsafe impl Send for LibraryHandle {}
762unsafe impl Sync for LibraryHandle {}
763
764impl LibraryHandle {
765    fn load(path: &Path, sandbox: bool) -> Result<Self, String> {
766        // Check if file exists
767        if !path.exists() {
768            return Err(format!("Library file not found: {:?}", path));
769        }
770
771        #[cfg(unix)]
772        {
773            UnixLibrary::load(path, sandbox).map(LibraryHandle::Unix)
774        }
775
776        #[cfg(windows)]
777        {
778            WindowsLibrary::load(path, sandbox).map(LibraryHandle::Windows)
779        }
780
781        #[cfg(not(any(unix, windows)))]
782        {
783            let _ = (path, sandbox);
784            Ok(LibraryHandle::Stub)
785        }
786    }
787
788    fn get_plugin_factory(&self) -> Result<Arc<dyn AstPlugin>, String> {
789        match self {
790            #[cfg(unix)]
791            LibraryHandle::Unix(lib) => lib.get_plugin_factory(),
792            #[cfg(windows)]
793            LibraryHandle::Windows(lib) => lib.get_plugin_factory(),
794            LibraryHandle::Stub => {
795                Err("Dynamic loading not supported on this platform".to_string())
796            }
797        }
798    }
799}
800
801#[cfg(unix)]
802struct UnixLibrary {
803    _handle: *mut std::ffi::c_void,
804}
805
806#[cfg(unix)]
807impl UnixLibrary {
808    fn load(path: &Path, sandbox: bool) -> Result<Self, String> {
809        use std::ffi::CString;
810
811        // Convert path to C string
812        let path_str = path.to_str().ok_or("Invalid path")?;
813        let c_path = CString::new(path_str).map_err(|e| e.to_string())?;
814
815        // Load library with dlopen
816        let flags = if sandbox {
817            0x0002 | 0x0100 // RTLD_NOW | RTLD_LOCAL
818        } else {
819            0x0002 // RTLD_NOW
820        };
821
822        unsafe {
823            let handle = libc::dlopen(c_path.as_ptr(), flags);
824            if handle.is_null() {
825                let error = std::ffi::CStr::from_ptr(libc::dlerror());
826                return Err(format!("Failed to load library: {:?}", error));
827            }
828
829            Ok(Self { _handle: handle })
830        }
831    }
832
833    fn get_plugin_factory(&self) -> Result<Arc<dyn AstPlugin>, String> {
834        use std::ffi::{CStr, CString};
835
836        type PluginFactory = unsafe extern "C" fn() -> *mut std::ffi::c_void;
837
838        let symbol_name = CString::new("pdf_ast_plugin_factory")
839            .map_err(|e| format!("Invalid symbol name: {}", e))?;
840
841        unsafe {
842            let symbol = libc::dlsym(self._handle, symbol_name.as_ptr());
843            if symbol.is_null() {
844                let error = libc::dlerror();
845                let message = if error.is_null() {
846                    "Failed to resolve symbol".to_string()
847                } else {
848                    CStr::from_ptr(error).to_string_lossy().into_owned()
849                };
850                return Err(format!("Failed to resolve plugin factory: {}", message));
851            }
852
853            let factory: PluginFactory = std::mem::transmute(symbol);
854            let plugin_ptr = factory();
855            if plugin_ptr.is_null() {
856                return Err("Plugin factory returned null".to_string());
857            }
858            let boxed_plugin = Box::from_raw(plugin_ptr as *mut Box<dyn AstPlugin>);
859            Ok(Arc::from(*boxed_plugin))
860        }
861    }
862}
863
864#[cfg(unix)]
865impl Drop for UnixLibrary {
866    fn drop(&mut self) {
867        unsafe {
868            libc::dlclose(self._handle);
869        }
870    }
871}
872
873#[cfg(windows)]
874struct WindowsLibrary {
875    _handle: *mut std::ffi::c_void,
876}
877
878#[cfg(windows)]
879impl WindowsLibrary {
880    fn load(path: &Path, _sandbox: bool) -> Result<Self, String> {
881        use std::ffi::OsStr;
882        use std::os::windows::ffi::OsStrExt;
883
884        // Convert path to wide string
885        let wide_path: Vec<u16> = OsStr::new(path.as_os_str())
886            .encode_wide()
887            .chain(Some(0))
888            .collect();
889
890        unsafe {
891            let handle = LoadLibraryW(wide_path.as_ptr());
892            if handle.is_null() {
893                return Err("Failed to load library".to_string());
894            }
895
896            Ok(Self { _handle: handle })
897        }
898    }
899
900    fn get_plugin_factory(&self) -> Result<Arc<dyn AstPlugin>, String> {
901        use std::ffi::CString;
902
903        type PluginFactory = unsafe extern "C" fn() -> *mut std::ffi::c_void;
904
905        let symbol_name = CString::new("pdf_ast_plugin_factory")
906            .map_err(|e| format!("Invalid symbol name: {}", e))?;
907
908        unsafe {
909            let symbol = GetProcAddress(self._handle, symbol_name.as_ptr());
910            if symbol.is_null() {
911                return Err("Failed to resolve plugin factory".to_string());
912            }
913            let factory: PluginFactory = std::mem::transmute(symbol);
914            let plugin_ptr = factory();
915            if plugin_ptr.is_null() {
916                return Err("Plugin factory returned null".to_string());
917            }
918            let boxed_plugin = Box::from_raw(plugin_ptr as *mut Box<dyn AstPlugin>);
919            Ok(Arc::from(*boxed_plugin))
920        }
921    }
922}
923
924#[cfg(windows)]
925impl Drop for WindowsLibrary {
926    fn drop(&mut self) {
927        unsafe {
928            FreeLibrary(self._handle);
929        }
930    }
931}
932
933// FFI declarations for Unix
934#[cfg(unix)]
935#[allow(dead_code)]
936extern "C" {
937    fn dlopen(filename: *const std::ffi::c_char, flag: std::ffi::c_int) -> *mut std::ffi::c_void;
938    fn dlerror() -> *const std::ffi::c_char;
939}
940
941// FFI declarations for Windows
942#[cfg(windows)]
943extern "system" {
944    fn LoadLibraryW(lpFileName: *const u16) -> *mut std::ffi::c_void;
945    fn GetProcAddress(
946        hModule: *mut std::ffi::c_void,
947        lpProcName: *const i8,
948    ) -> *mut std::ffi::c_void;
949    fn FreeLibrary(hModule: *mut std::ffi::c_void) -> i32;
950}