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_python_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_python_package(&self, dir: &Path, plugins: &mut Vec<PluginInfo>) {
174 let Ok(manifest) = fidius_core::package::load_manifest_untyped(dir) else {
175 return;
176 };
177 if !matches!(
178 manifest.package.runtime(),
179 fidius_core::package::PackageRuntime::Python
180 ) {
181 return;
182 }
183 plugins.push(PluginInfo {
184 name: manifest.package.name.clone(),
185 interface_name: manifest.package.interface.clone(),
186 interface_hash: 0,
190 interface_version: manifest.package.interface_version,
191 capabilities: 0,
192 buffer_strategy: BufferStrategyKind::PluginAllocated,
193 runtime: PluginRuntimeKind::Python,
194 });
195 }
196
197 pub fn load(&self, name: &str) -> Result<LoadedPlugin, LoadError> {
202 #[cfg(feature = "tracing")]
203 tracing::info!(plugin_name = name, "loading plugin");
204
205 for search_path in &self.search_paths {
206 if !search_path.is_dir() {
207 continue;
208 }
209
210 let entries = std::fs::read_dir(search_path)?;
211 for entry in entries {
212 let entry = entry?;
213 let path = entry.path();
214
215 if !is_dylib(&path) {
216 continue;
217 }
218
219 if self.require_signature {
221 signing::verify_signature(&path, &self.trusted_keys)?;
222 }
223
224 match loader::load_library(&path) {
225 Ok(loaded) => {
226 for plugin in loaded.plugins {
227 if plugin.info.name == name {
228 loader::validate_against_interface(
229 &plugin,
230 self.expected_hash,
231 self.expected_strategy,
232 )?;
233 return Ok(plugin);
234 }
235 }
236 }
237 Err(_) => continue,
238 }
239 }
240 }
241
242 Err(LoadError::PluginNotFound {
243 name: name.to_string(),
244 })
245 }
246
247 pub fn find_python_package(&self, name: &str) -> Result<PathBuf, LoadError> {
251 for search_path in &self.search_paths {
252 if !search_path.is_dir() {
253 continue;
254 }
255 let entries = std::fs::read_dir(search_path)?;
256 for entry in entries {
257 let entry = entry?;
258 let path = entry.path();
259 if !path.is_dir() {
260 continue;
261 }
262 if !path.join("package.toml").exists() {
263 continue;
264 }
265 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
266 continue;
267 };
268 if matches!(
269 manifest.package.runtime(),
270 fidius_core::package::PackageRuntime::Python
271 ) && manifest.package.name == name
272 {
273 return Ok(path);
274 }
275 }
276 }
277 Err(LoadError::PluginNotFound {
278 name: name.to_string(),
279 })
280 }
281
282 #[cfg(feature = "python")]
292 pub fn load_python(
293 &self,
294 name: &str,
295 descriptor: &'static fidius_core::python_descriptor::PythonInterfaceDescriptor,
296 ) -> Result<fidius_python::PythonPluginHandle, LoadError> {
297 let dir = self.find_python_package(name)?;
298 fidius_python::load_python_plugin(&dir, descriptor)
299 .map_err(|e| LoadError::PythonLoad(e.to_string()))
300 }
301}
302
303fn is_dylib(path: &Path) -> bool {
305 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
306 if cfg!(target_os = "macos") {
307 ext == "dylib"
308 } else if cfg!(target_os = "windows") {
309 ext == "dll"
310 } else {
311 ext == "so"
312 }
313}