1use std::path::{Path, PathBuf};
18
19use ed25519_dalek::VerifyingKey;
20use fidius_core::descriptor::BufferStrategyKind;
21
22use crate::error::LoadError;
23use crate::loader::{self, LoadedPlugin};
24use crate::signing;
25use crate::types::{LoadPolicy, PluginInfo, PluginRuntimeKind};
26
27#[allow(dead_code)] pub struct PluginHost {
30 search_paths: Vec<PathBuf>,
31 load_policy: LoadPolicy,
32 require_signature: bool,
33 trusted_keys: Vec<VerifyingKey>,
34 expected_hash: Option<u64>,
35 expected_strategy: Option<BufferStrategyKind>,
36}
37
38pub struct PluginHostBuilder {
40 search_paths: Vec<PathBuf>,
41 load_policy: LoadPolicy,
42 require_signature: bool,
43 trusted_keys: Vec<VerifyingKey>,
44 expected_hash: Option<u64>,
45 expected_strategy: Option<BufferStrategyKind>,
46}
47
48impl PluginHostBuilder {
49 fn new() -> Self {
50 Self {
51 search_paths: Vec::new(),
52 load_policy: LoadPolicy::Strict,
53 require_signature: false,
54 trusted_keys: Vec::new(),
55 expected_hash: None,
56 expected_strategy: None,
57 }
58 }
59
60 pub fn search_path(mut self, path: impl Into<PathBuf>) -> Self {
62 self.search_paths.push(path.into());
63 self
64 }
65
66 pub fn load_policy(mut self, policy: LoadPolicy) -> Self {
68 self.load_policy = policy;
69 self
70 }
71
72 pub fn require_signature(mut self, require: bool) -> Self {
74 self.require_signature = require;
75 self
76 }
77
78 pub fn trusted_keys(mut self, keys: &[VerifyingKey]) -> Self {
80 self.trusted_keys = keys.to_vec();
81 self
82 }
83
84 pub fn interface_hash(mut self, hash: u64) -> Self {
86 self.expected_hash = Some(hash);
87 self
88 }
89
90 pub fn buffer_strategy(mut self, strategy: BufferStrategyKind) -> Self {
92 self.expected_strategy = Some(strategy);
93 self
94 }
95
96 pub fn build(self) -> Result<PluginHost, LoadError> {
98 Ok(PluginHost {
99 search_paths: self.search_paths,
100 load_policy: self.load_policy,
101 require_signature: self.require_signature,
102 trusted_keys: self.trusted_keys,
103 expected_hash: self.expected_hash,
104 expected_strategy: self.expected_strategy,
105 })
106 }
107}
108
109impl PluginHost {
110 pub fn builder() -> PluginHostBuilder {
112 PluginHostBuilder::new()
113 }
114
115 pub fn discover(&self) -> Result<Vec<PluginInfo>, LoadError> {
125 #[cfg(feature = "tracing")]
126 tracing::info!(search_paths = ?self.search_paths, "discovering plugins");
127
128 let mut plugins = Vec::new();
129
130 for search_path in &self.search_paths {
131 if !search_path.is_dir() {
132 continue;
133 }
134
135 let entries = std::fs::read_dir(search_path)?;
136 for entry in entries {
137 let entry = entry?;
138 let path = entry.path();
139
140 if is_dylib(&path) {
141 self.discover_cdylib(&path, &mut plugins);
142 } else if path.is_dir() && path.join("package.toml").exists() {
143 self.discover_package(&path, &mut plugins);
144 }
145 }
146 }
147
148 Ok(plugins)
149 }
150
151 fn discover_cdylib(&self, path: &Path, plugins: &mut Vec<PluginInfo>) {
152 if self.require_signature && signing::verify_signature(path, &self.trusted_keys).is_err() {
154 return;
155 }
156
157 let Ok(loaded) = loader::load_library(path) else {
158 return; };
160 for plugin in &loaded.plugins {
161 if loader::validate_against_interface(
162 plugin,
163 self.expected_hash,
164 self.expected_strategy,
165 )
166 .is_ok()
167 {
168 plugins.push(plugin.info.clone());
169 }
170 }
171 }
172
173 fn discover_package(&self, dir: &Path, plugins: &mut Vec<PluginInfo>) {
177 let Ok(manifest) = fidius_core::package::load_manifest_untyped(dir) else {
178 return;
179 };
180 use fidius_core::package::PackageRuntime;
181 let runtime = match manifest.package.runtime() {
182 PackageRuntime::Python => PluginRuntimeKind::Python,
183 PackageRuntime::Wasm => PluginRuntimeKind::Wasm,
184 PackageRuntime::Rust => return,
187 };
188 plugins.push(PluginInfo {
189 name: manifest.package.name.clone(),
190 interface_name: manifest.package.interface.clone(),
191 interface_hash: 0,
195 interface_version: manifest.package.interface_version,
196 capabilities: 0,
197 buffer_strategy: BufferStrategyKind::PluginAllocated,
198 runtime,
199 });
200 }
201
202 pub fn load(&self, name: &str) -> Result<LoadedPlugin, LoadError> {
207 #[cfg(feature = "tracing")]
208 tracing::info!(plugin_name = name, "loading plugin");
209
210 for search_path in &self.search_paths {
211 if !search_path.is_dir() {
212 continue;
213 }
214
215 let entries = std::fs::read_dir(search_path)?;
216 for entry in entries {
217 let entry = entry?;
218 let path = entry.path();
219
220 if !is_dylib(&path) {
221 continue;
222 }
223
224 if self.require_signature {
226 signing::verify_signature(&path, &self.trusted_keys)?;
227 }
228
229 match loader::load_library(&path) {
230 Ok(loaded) => {
231 for plugin in loaded.plugins {
232 if plugin.info.name == name {
233 loader::validate_against_interface(
234 &plugin,
235 self.expected_hash,
236 self.expected_strategy,
237 )?;
238 return Ok(plugin);
239 }
240 }
241 }
242 Err(_) => continue,
243 }
244 }
245 }
246
247 Err(LoadError::PluginNotFound {
248 name: name.to_string(),
249 })
250 }
251
252 pub fn find_python_package(&self, name: &str) -> Result<PathBuf, LoadError> {
256 for search_path in &self.search_paths {
257 if !search_path.is_dir() {
258 continue;
259 }
260 let entries = std::fs::read_dir(search_path)?;
261 for entry in entries {
262 let entry = entry?;
263 let path = entry.path();
264 if !path.is_dir() {
265 continue;
266 }
267 if !path.join("package.toml").exists() {
268 continue;
269 }
270 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
271 continue;
272 };
273 if matches!(
274 manifest.package.runtime(),
275 fidius_core::package::PackageRuntime::Python
276 ) && manifest.package.name == name
277 {
278 return Ok(path);
279 }
280 }
281 }
282 Err(LoadError::PluginNotFound {
283 name: name.to_string(),
284 })
285 }
286
287 #[cfg(feature = "python")]
297 pub fn load_python(
298 &self,
299 name: &str,
300 descriptor: &'static fidius_core::python_descriptor::PythonInterfaceDescriptor,
301 ) -> Result<crate::handle::PluginHandle, LoadError> {
302 let dir = self.find_python_package(name)?;
303 if self.require_signature {
305 signing::verify_package_signature(&dir, &self.trusted_keys)?;
306 }
307 let manifest = fidius_core::package::load_manifest_untyped(&dir)
308 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
309 let py = fidius_python::load_python_plugin(&dir, descriptor)
310 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
311 let info = crate::types::PluginInfo {
315 name: manifest.package.name.clone(),
316 interface_name: descriptor.interface_name.to_string(),
317 interface_hash: descriptor.interface_hash,
318 interface_version: manifest.package.interface_version,
319 capabilities: 0,
320 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
321 runtime: crate::types::PluginRuntimeKind::Python,
322 };
323 Ok(crate::handle::PluginHandle::from_python(py, info))
324 }
325
326 #[cfg(feature = "wasm")]
329 pub fn find_wasm_package(&self, name: &str) -> Result<PathBuf, LoadError> {
330 for search_path in &self.search_paths {
331 if !search_path.is_dir() {
332 continue;
333 }
334 for entry in std::fs::read_dir(search_path)? {
335 let entry = entry?;
336 let path = entry.path();
337 if !path.is_dir() || !path.join("package.toml").exists() {
338 continue;
339 }
340 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
341 continue;
342 };
343 if matches!(
344 manifest.package.runtime(),
345 fidius_core::package::PackageRuntime::Wasm
346 ) && manifest.package.name == name
347 {
348 return Ok(path);
349 }
350 }
351 }
352 Err(LoadError::PluginNotFound {
353 name: name.to_string(),
354 })
355 }
356
357 #[cfg(feature = "wasm")]
367 pub fn load_wasm(
368 &self,
369 name: &str,
370 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
371 ) -> Result<crate::handle::PluginHandle, LoadError> {
372 use crate::executor::wasm::{WasmComponentExecutor, WasmMethod};
373
374 let dir = self.find_wasm_package(name)?;
375 if self.require_signature {
377 signing::verify_package_signature(&dir, &self.trusted_keys)?;
378 }
379 let manifest = fidius_core::package::load_manifest_untyped(&dir)
380 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
381 let wasm_meta = manifest
382 .wasm
383 .as_ref()
384 .ok_or_else(|| LoadError::WasmLoad("manifest is missing the [wasm] section".into()))?;
385
386 let methods: Vec<WasmMethod> = descriptor
387 .methods
388 .iter()
389 .map(|m| WasmMethod {
390 name: m.name.to_string(),
391 wire_raw: m.wire_raw,
392 })
393 .collect();
394 let info = crate::types::PluginInfo {
395 name: manifest.package.name.clone(),
396 interface_name: descriptor.interface_name.to_string(),
397 interface_hash: descriptor.interface_hash,
398 interface_version: manifest.package.interface_version,
399 capabilities: 0,
400 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
401 runtime: crate::types::PluginRuntimeKind::Wasm,
402 };
403 let interface = descriptor.interface_export.to_string();
404 let capabilities = wasm_meta.capabilities.clone();
405
406 let cwasm_path = wasm_meta
412 .precompiled
413 .as_ref()
414 .map(|p| dir.join(p))
415 .or_else(|| {
416 let sibling = dir.join(&wasm_meta.component).with_extension("cwasm");
417 sibling.exists().then_some(sibling)
418 });
419
420 let jit = |interface: String, methods, capabilities, info| -> Result<_, LoadError> {
421 let bytes = std::fs::read(dir.join(&wasm_meta.component))?;
422 WasmComponentExecutor::from_component_bytes(
423 &bytes,
424 interface,
425 methods,
426 capabilities,
427 info,
428 )
429 .map_err(|e| LoadError::WasmLoad(e.to_string()))
430 };
431
432 let executor = match cwasm_path {
433 Some(cwasm) if cwasm.exists() => {
434 let bytes = std::fs::read(&cwasm)?;
435 let aot = unsafe {
439 WasmComponentExecutor::from_cwasm(
440 &bytes,
441 interface.clone(),
442 methods.clone(),
443 capabilities.clone(),
444 info.clone(),
445 )
446 };
447 match aot {
448 Ok(e) => e,
449 Err(_err) => {
450 #[cfg(feature = "tracing")]
451 tracing::warn!(
452 cwasm = %cwasm.display(),
453 error = %_err,
454 "precompiled .cwasm rejected (likely engine/version mismatch); falling back to JIT"
455 );
456 jit(interface, methods, capabilities, info)?
457 }
458 }
459 }
460 _ => jit(interface, methods, capabilities, info)?,
461 };
462
463 let got = executor
465 .interface_hash()
466 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
467 if got != descriptor.interface_hash {
468 return Err(LoadError::InterfaceHashMismatch {
469 got,
470 expected: descriptor.interface_hash,
471 });
472 }
473
474 Ok(crate::handle::PluginHandle::from_wasm(executor))
475 }
476}
477
478fn is_dylib(path: &Path) -> bool {
480 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
481 if cfg!(target_os = "macos") {
482 ext == "dylib"
483 } else if cfg!(target_os = "windows") {
484 ext == "dll"
485 } else {
486 ext == "so"
487 }
488}