1use std::path::{Path, PathBuf};
18
19use ed25519_dalek::VerifyingKey;
20use fidius_core::descriptor::{BufferStrategyKind, WireFormat};
21
22use crate::error::LoadError;
23use crate::loader::{self, LoadedPlugin};
24use crate::signing;
25use crate::types::{LoadPolicy, PluginInfo};
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_wire: Option<WireFormat>,
36 expected_strategy: Option<BufferStrategyKind>,
37}
38
39pub struct PluginHostBuilder {
41 search_paths: Vec<PathBuf>,
42 load_policy: LoadPolicy,
43 require_signature: bool,
44 trusted_keys: Vec<VerifyingKey>,
45 expected_hash: Option<u64>,
46 expected_wire: Option<WireFormat>,
47 expected_strategy: Option<BufferStrategyKind>,
48}
49
50impl PluginHostBuilder {
51 fn new() -> Self {
52 Self {
53 search_paths: Vec::new(),
54 load_policy: LoadPolicy::Strict,
55 require_signature: false,
56 trusted_keys: Vec::new(),
57 expected_hash: None,
58 expected_wire: None,
59 expected_strategy: None,
60 }
61 }
62
63 pub fn search_path(mut self, path: impl Into<PathBuf>) -> Self {
65 self.search_paths.push(path.into());
66 self
67 }
68
69 pub fn load_policy(mut self, policy: LoadPolicy) -> Self {
71 self.load_policy = policy;
72 self
73 }
74
75 pub fn require_signature(mut self, require: bool) -> Self {
77 self.require_signature = require;
78 self
79 }
80
81 pub fn trusted_keys(mut self, keys: &[VerifyingKey]) -> Self {
83 self.trusted_keys = keys.to_vec();
84 self
85 }
86
87 pub fn interface_hash(mut self, hash: u64) -> Self {
89 self.expected_hash = Some(hash);
90 self
91 }
92
93 pub fn wire_format(mut self, format: WireFormat) -> Self {
95 self.expected_wire = Some(format);
96 self
97 }
98
99 pub fn buffer_strategy(mut self, strategy: BufferStrategyKind) -> Self {
101 self.expected_strategy = Some(strategy);
102 self
103 }
104
105 pub fn build(self) -> Result<PluginHost, LoadError> {
107 Ok(PluginHost {
108 search_paths: self.search_paths,
109 load_policy: self.load_policy,
110 require_signature: self.require_signature,
111 trusted_keys: self.trusted_keys,
112 expected_hash: self.expected_hash,
113 expected_wire: self.expected_wire,
114 expected_strategy: self.expected_strategy,
115 })
116 }
117}
118
119impl PluginHost {
120 pub fn builder() -> PluginHostBuilder {
122 PluginHostBuilder::new()
123 }
124
125 pub fn discover(&self) -> Result<Vec<PluginInfo>, LoadError> {
130 #[cfg(feature = "tracing")]
131 tracing::info!(search_paths = ?self.search_paths, "discovering plugins");
132
133 let mut plugins = Vec::new();
134
135 for search_path in &self.search_paths {
136 if !search_path.is_dir() {
137 continue;
138 }
139
140 let entries = std::fs::read_dir(search_path)?;
141 for entry in entries {
142 let entry = entry?;
143 let path = entry.path();
144
145 if !is_dylib(&path) {
146 continue;
147 }
148
149 if self.require_signature
151 && signing::verify_signature(&path, &self.trusted_keys).is_err()
152 {
153 continue;
154 }
155
156 match loader::load_library(&path) {
157 Ok(loaded) => {
158 for plugin in &loaded.plugins {
159 if let Ok(()) = loader::validate_against_interface(
160 plugin,
161 self.expected_hash,
162 self.expected_wire,
163 self.expected_strategy,
164 ) {
165 plugins.push(plugin.info.clone());
166 }
167 }
168 }
169 Err(_) => {
170 continue;
172 }
173 }
174 }
175 }
176
177 Ok(plugins)
178 }
179
180 pub fn load(&self, name: &str) -> Result<LoadedPlugin, LoadError> {
185 #[cfg(feature = "tracing")]
186 tracing::info!(plugin_name = name, "loading plugin");
187
188 for search_path in &self.search_paths {
189 if !search_path.is_dir() {
190 continue;
191 }
192
193 let entries = std::fs::read_dir(search_path)?;
194 for entry in entries {
195 let entry = entry?;
196 let path = entry.path();
197
198 if !is_dylib(&path) {
199 continue;
200 }
201
202 if self.require_signature {
204 signing::verify_signature(&path, &self.trusted_keys)?;
205 }
206
207 match loader::load_library(&path) {
208 Ok(loaded) => {
209 for plugin in loaded.plugins {
210 if plugin.info.name == name {
211 loader::validate_against_interface(
212 &plugin,
213 self.expected_hash,
214 self.expected_wire,
215 self.expected_strategy,
216 )?;
217 return Ok(plugin);
218 }
219 }
220 }
221 Err(_) => continue,
222 }
223 }
224 }
225
226 Err(LoadError::PluginNotFound {
227 name: name.to_string(),
228 })
229 }
230}
231
232fn is_dylib(path: &Path) -> bool {
234 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
235 if cfg!(target_os = "macos") {
236 ext == "dylib"
237 } else if cfg!(target_os = "windows") {
238 ext == "dll"
239 } else {
240 ext == "so"
241 }
242}