Skip to main content

fidius_host/
loader.rs

1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Core plugin loading and descriptor validation.
16
17use std::ffi::c_void;
18use std::path::Path;
19use std::sync::Arc;
20
21use fidius_core::descriptor::*;
22use libloading::Library;
23
24use crate::error::LoadError;
25use crate::types::PluginInfo;
26
27/// A loaded plugin library with validated descriptors.
28pub struct LoadedLibrary {
29    /// The dynamically loaded library. Must stay alive while any PluginHandle exists.
30    pub library: Arc<Library>,
31    /// Validated plugin descriptors with owned metadata.
32    pub plugins: Vec<LoadedPlugin>,
33}
34
35/// A single validated plugin from a loaded library.
36pub struct LoadedPlugin {
37    /// Owned metadata copied from the FFI descriptor.
38    pub info: PluginInfo,
39    /// Raw vtable pointer (points into the loaded library's memory).
40    pub vtable: *const c_void,
41    /// Free function for plugin-allocated buffers.
42    pub free_buffer: Option<unsafe extern "C" fn(*mut u8, usize)>,
43    /// Total number of methods in the vtable.
44    pub method_count: u32,
45    /// Reference to the library to keep it alive.
46    pub library: Arc<Library>,
47}
48
49// SAFETY: vtable and free_buffer point to static data in the loaded library.
50// The Arc<Library> ensures the library stays loaded.
51unsafe impl Send for LoadedPlugin {}
52unsafe impl Sync for LoadedPlugin {}
53
54impl std::fmt::Debug for LoadedPlugin {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("LoadedPlugin")
57            .field("info", &self.info)
58            .field("vtable", &self.vtable)
59            .finish()
60    }
61}
62
63/// Load a plugin library from a path.
64///
65/// Opens the dylib, calls `fidius_get_registry()`, validates the registry
66/// and all descriptors, copies FFI data to owned types.
67pub fn load_library(path: &Path) -> Result<LoadedLibrary, LoadError> {
68    let path_str = path.display().to_string();
69
70    #[cfg(feature = "tracing")]
71    tracing::debug!(path = %path_str, "loading library");
72
73    // Check architecture before dlopen
74    crate::arch::check_architecture(path)?;
75
76    // dlopen
77    let library = unsafe { Library::new(path) }.map_err(|e| {
78        if e.to_string().contains("No such file") || e.to_string().contains("not found") {
79            LoadError::LibraryNotFound {
80                path: path_str.clone(),
81            }
82        } else {
83            LoadError::LibLoading(e)
84        }
85    })?;
86
87    // dlsym("fidius_get_registry")
88    let get_registry: libloading::Symbol<unsafe extern "C" fn() -> *const PluginRegistry> =
89        unsafe { library.get(b"fidius_get_registry") }.map_err(|_| LoadError::SymbolNotFound {
90            path: path_str.clone(),
91        })?;
92
93    // Call to get the registry pointer
94    let registry = unsafe { &*get_registry() };
95
96    // Validate magic
97    if registry.magic != FIDIUS_MAGIC {
98        return Err(LoadError::InvalidMagic);
99    }
100
101    // Validate registry version
102    if registry.registry_version != REGISTRY_VERSION {
103        return Err(LoadError::IncompatibleRegistryVersion {
104            got: registry.registry_version,
105            expected: REGISTRY_VERSION,
106        });
107    }
108
109    let library = Arc::new(library);
110
111    // Iterate descriptors and validate each
112    let mut plugins = Vec::with_capacity(registry.plugin_count as usize);
113    for i in 0..registry.plugin_count {
114        let desc = unsafe { &**registry.descriptors.add(i as usize) };
115        let plugin = validate_descriptor(desc, &library)?;
116        plugins.push(plugin);
117    }
118
119    Ok(LoadedLibrary { library, plugins })
120}
121
122/// Validate a single descriptor and copy to owned types.
123fn validate_descriptor(
124    desc: &PluginDescriptor,
125    library: &Arc<Library>,
126) -> Result<LoadedPlugin, LoadError> {
127    // Check ABI version
128    if desc.abi_version != ABI_VERSION {
129        return Err(LoadError::IncompatibleAbiVersion {
130            got: desc.abi_version,
131            expected: ABI_VERSION,
132        });
133    }
134
135    // Copy FFI strings to owned
136    let interface_name = unsafe { desc.interface_name_str() }.to_string();
137    let plugin_name = unsafe { desc.plugin_name_str() }.to_string();
138
139    let info = PluginInfo {
140        name: plugin_name,
141        interface_name,
142        interface_hash: desc.interface_hash,
143        interface_version: desc.interface_version,
144        capabilities: desc.capabilities,
145        wire_format: desc
146            .wire_format_kind()
147            .map_err(|v| LoadError::UnknownWireFormat { value: v })?,
148        buffer_strategy: desc
149            .buffer_strategy_kind()
150            .map_err(|v| LoadError::UnknownBufferStrategy { value: v })?,
151    };
152
153    Ok(LoadedPlugin {
154        info,
155        vtable: desc.vtable,
156        free_buffer: desc.free_buffer,
157        method_count: desc.method_count,
158        library: Arc::clone(library),
159    })
160}
161
162/// Validate a loaded plugin against expected interface parameters.
163pub fn validate_against_interface(
164    plugin: &LoadedPlugin,
165    expected_hash: Option<u64>,
166    expected_wire: Option<WireFormat>,
167    expected_strategy: Option<BufferStrategyKind>,
168) -> Result<(), LoadError> {
169    if let Some(hash) = expected_hash {
170        if plugin.info.interface_hash != hash {
171            return Err(LoadError::InterfaceHashMismatch {
172                got: plugin.info.interface_hash,
173                expected: hash,
174            });
175        }
176    }
177
178    if let Some(wire) = expected_wire {
179        if plugin.info.wire_format != wire {
180            return Err(LoadError::WireFormatMismatch {
181                got: plugin.info.wire_format,
182                expected: wire,
183            });
184        }
185    }
186
187    if let Some(strategy) = expected_strategy {
188        if plugin.info.buffer_strategy != strategy {
189            return Err(LoadError::BufferStrategyMismatch {
190                got: plugin.info.buffer_strategy,
191                expected: strategy,
192            });
193        }
194    }
195
196    Ok(())
197}