1use std::collections::BTreeMap;
17use std::ffi::CStr;
18use std::os::raw::c_char;
19use std::path::Path;
20
21use libloading::Library;
22use snafu::{ResultExt, Snafu};
23
24use crate::cli::{parse_algorithm_overrides, parse_interface_overrides};
25
26#[repr(C)]
29pub struct CFMPluginMetadata {
30 pub name: *const c_char,
31 pub version: *const c_char,
32 pub vendor: *const c_char,
33 pub license: *const c_char,
34 pub homepage: *const c_char,
35 pub description: *const c_char,
36 pub homepage_url: *const c_char,
37 pub source_url: *const c_char,
38 pub issue_tracker_url: *const c_char,
39}
40
41#[derive(Debug, Default, Clone, PartialEq, Eq)]
45pub struct PluginMetadata {
46 pub name: Option<String>,
47 pub version: Option<String>,
48 pub vendor: Option<String>,
49 pub license: Option<String>,
50 pub homepage: Option<String>,
51 pub description: Option<String>,
52 pub source_url: Option<String>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct InterfaceEntry {
59 pub name: String,
60 pub version: u8,
61}
62
63#[derive(Snafu, Debug)]
64pub enum LoadError {
65 #[snafu(display("failed to open artifact at '{}'", path.display()))]
66 OpenLibrary {
67 path: Box<Path>,
68 source: libloading::Error,
69 },
70
71 #[snafu(display("invalid UTF-8 in FFI payload"))]
72 InvalidUtf8 { source: std::str::Utf8Error },
73
74 #[snafu(display("invalid CLI override: {}", message))]
75 InvalidOverride { message: String },
76}
77
78impl LoadError {
79 fn from_override(message: String) -> Self {
80 Self::InvalidOverride { message }
81 }
82}
83
84pub type Result<T> = std::result::Result<T, LoadError>;
85
86pub fn open_library(path: &Path) -> Result<Library> {
89 let path_boxed: Box<Path> = Box::from(path);
90 unsafe { Library::new(path_boxed.as_ref()) }.context(OpenLibrarySnafu { path: path_boxed })
91}
92
93pub fn query_metadata(lib: &Library) -> Result<Option<PluginMetadata>> {
97 let Ok(sym) =
98 (unsafe { lib.get::<extern "C" fn() -> *const CFMPluginMetadata>(b"cfmp_metadata\0") })
99 else {
100 return Ok(None);
101 };
102 let raw_ptr = sym();
103 if raw_ptr.is_null() {
104 return Ok(None);
105 }
106 let raw = unsafe { &*raw_ptr };
110 Ok(Some(PluginMetadata {
111 name: cstr_to_string(raw.name),
112 version: cstr_to_string(raw.version),
113 vendor: cstr_to_string(raw.vendor),
114 license: cstr_to_string(raw.license),
115 homepage: cstr_to_string(raw.homepage),
116 description: cstr_to_string(raw.description),
117 source_url: cstr_to_string(raw.source_url),
118 }))
119}
120
121pub fn query_interfaces(lib: &Library) -> Result<Option<Vec<InterfaceEntry>>> {
131 let Ok(sym) = (unsafe {
132 lib.get::<extern "C" fn(*mut std::ffi::c_void) -> *const u8>(b"cfmp_query_interfaces\0")
133 }) else {
134 return Ok(None);
135 };
136 let ptr = sym(std::ptr::null_mut());
137 if ptr.is_null() {
138 return Ok(Some(Vec::new()));
139 }
140 Ok(Some(parse_interface_stream(ptr)?))
141}
142
143fn parse_interface_stream(start: *const u8) -> Result<Vec<InterfaceEntry>> {
146 let mut out = Vec::new();
147 let mut idx = 0usize;
148 loop {
149 let name_start = idx;
150 let mut name_end = name_start;
151 while unsafe { *start.add(name_end) } != 0 {
156 name_end += 1;
157 }
158 let bytes =
159 unsafe { std::slice::from_raw_parts(start.add(name_start), name_end - name_start) };
160 let name = std::str::from_utf8(bytes).context(InvalidUtf8Snafu)?;
161 if name.is_empty() {
162 break;
163 }
164 let version = unsafe { *start.add(name_end + 1) };
165 out.push(InterfaceEntry {
166 name: name.to_string(),
167 version,
168 });
169 idx = name_end + 2;
170 }
171 Ok(out)
172}
173
174fn cstr_to_string(ptr: *const c_char) -> Option<String> {
175 if ptr.is_null() {
176 return None;
177 }
178 let bytes = unsafe { CStr::from_ptr(ptr) }.to_bytes();
181 std::str::from_utf8(bytes).ok().map(str::to_string)
182}
183
184pub fn resolve_interfaces(
188 ffi: Option<&[InterfaceEntry]>,
189 cli_override: Option<&[String]>,
190) -> Result<BTreeMap<String, u8>> {
191 if let Some(raw) = cli_override {
192 let parsed = parse_interface_overrides(raw).map_err(LoadError::from_override)?;
193 let mut map = BTreeMap::new();
194 for (name, ver) in parsed {
195 map.insert(name, ver);
196 }
197 return Ok(map);
198 }
199 let mut map: BTreeMap<String, u8> = BTreeMap::new();
200 if let Some(entries) = ffi {
201 for entry in entries {
202 map.entry(entry.name.clone())
203 .and_modify(|v: &mut u8| *v = (*v).max(entry.version))
204 .or_insert(entry.version);
205 }
206 }
207 Ok(map)
208}
209
210pub fn resolve_algorithms(
213 cli_override: Option<&[String]>,
214) -> Result<BTreeMap<String, Vec<String>>> {
215 let mut map = BTreeMap::new();
216 if let Some(raw) = cli_override {
217 let parsed = parse_algorithm_overrides(raw).map_err(LoadError::from_override)?;
218 for (iface, algos) in parsed {
219 map.insert(iface, algos);
220 }
221 }
222 Ok(map)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn resolve_interfaces_prefers_cli_override() {
231 let ffi = vec![
232 InterfaceEntry {
233 name: "hash".into(),
234 version: 0,
235 },
236 InterfaceEntry {
237 name: "rng".into(),
238 version: 0,
239 },
240 ];
241 let cli = vec!["aead:1".to_string()];
242 let got = resolve_interfaces(Some(&ffi), Some(&cli)).unwrap();
243 assert_eq!(got.len(), 1);
245 assert_eq!(got["aead"], 1);
246 }
247
248 #[test]
249 fn resolve_interfaces_falls_back_to_ffi() {
250 let ffi = vec![InterfaceEntry {
251 name: "hash".into(),
252 version: 0,
253 }];
254 let got = resolve_interfaces(Some(&ffi), None).unwrap();
255 assert_eq!(got["hash"], 0);
256 }
257
258 #[test]
259 fn resolve_interfaces_empty_when_no_source() {
260 let got = resolve_interfaces(None, None).unwrap();
261 assert!(got.is_empty());
262 }
263
264 #[test]
265 fn resolve_algorithms_parses_cli() {
266 let cli = vec!["hash:SHA-256;SHA-512".to_string()];
267 let got = resolve_algorithms(Some(&cli)).unwrap();
268 assert_eq!(
269 got["hash"],
270 vec!["SHA-256".to_string(), "SHA-512".to_string()]
271 );
272 }
273}