linkrs_query/executor/expression/functions/udf/
loader.rs1use super::dynamic::DynamicPlugin;
4use super::error::UdfError;
5use super::plugin::{
6 SharedPlugin, UdfAbiVersionFn, UdfCreateFn, UdfDestroyFn, UdfPlugin, UDF_ABI_VERSION,
7 UDF_ABI_VERSION_SYMBOL, UDF_CREATE_SYMBOL, UDF_DESTROY_SYMBOL,
8};
9use libloading::{Library, Symbol};
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::time::SystemTime;
13
14pub struct LoadedPlugin {
16 pub plugin: SharedPlugin,
18 pub path: PathBuf,
20 pub loaded_mtime: Option<SystemTime>,
22}
23
24impl std::fmt::Debug for LoadedPlugin {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("LoadedPlugin")
27 .field("name", &self.plugin.name())
28 .field("path", &self.path)
29 .finish()
30 }
31}
32
33pub struct UdfLoader;
35
36impl UdfLoader {
37 pub fn install_from_source(source: &str) -> Result<LoadedPlugin, UdfError> {
43 let lower = source.to_ascii_lowercase();
44 if lower.starts_with("http://") || lower.starts_with("https://") {
45 return Err(UdfError::RepoDownloadUnsupported(source.to_string()));
46 }
47 Self::load(Path::new(source))
48 }
49
50 pub fn load(path: &Path) -> Result<LoadedPlugin, UdfError> {
52 let canonical = Self::validate_path(path)?;
53 let path_display = canonical.to_string_lossy().to_string();
54 let loaded_mtime = std::fs::metadata(&canonical)
55 .ok()
56 .and_then(|m| m.modified().ok());
57
58 let lib = unsafe { Library::new(&canonical) }
59 .map_err(|e| UdfError::LibraryLoad(path_display.clone(), e.to_string()))?;
60 let lib = Arc::new(lib);
61
62 let create: UdfCreateFn = unsafe {
65 let symbol: Symbol<UdfCreateFn> =
66 lib.get(UDF_CREATE_SYMBOL.as_bytes()).map_err(|_| {
67 UdfError::SymbolNotFound(path_display.clone(), UDF_CREATE_SYMBOL.into())
68 })?;
69 *symbol
70 };
71 let destroy: UdfDestroyFn = unsafe {
72 let symbol: Symbol<UdfDestroyFn> =
73 lib.get(UDF_DESTROY_SYMBOL.as_bytes()).map_err(|_| {
74 UdfError::SymbolNotFound(path_display.clone(), UDF_DESTROY_SYMBOL.into())
75 })?;
76 *symbol
77 };
78
79 if let Ok(symbol) = unsafe { lib.get::<UdfAbiVersionFn>(UDF_ABI_VERSION_SYMBOL.as_bytes()) }
81 {
82 let version_fn: UdfAbiVersionFn = *symbol;
83 let reported = unsafe { version_fn() };
84 if reported != UDF_ABI_VERSION {
85 return Err(UdfError::VersionMismatch(
86 path_display,
87 UDF_ABI_VERSION,
88 reported,
89 ));
90 }
91 }
92
93 let raw = unsafe { create() };
94 let owned = unsafe {
95 DynamicPlugin::from_raw(raw, destroy, Arc::clone(&lib), path_display.clone())
96 }?;
97 if owned.name().trim().is_empty() {
98 return Err(UdfError::InvalidName(path_display));
99 }
100 if owned.min_arity() > owned.max_arity() {
101 return Err(UdfError::InvalidName(format!(
102 "{path_display}: min_arity exceeds max_arity"
103 )));
104 }
105
106 Ok(LoadedPlugin {
107 plugin: Arc::new(owned),
108 path: canonical,
109 loaded_mtime,
110 })
111 }
112
113 fn validate_path(path: &Path) -> Result<PathBuf, UdfError> {
115 let display = path.to_string_lossy().to_string();
116 if !path.exists() {
117 return Err(UdfError::InvalidPath(
118 display,
119 "file does not exist".to_string(),
120 ));
121 }
122 let expected = Self::expected_extension();
123 let actual = path
124 .extension()
125 .map(|e| e.to_string_lossy().to_ascii_lowercase())
126 .unwrap_or_default();
127 if actual != expected {
128 return Err(UdfError::InvalidPath(
129 display,
130 format!("expected a '*.{expected}' dynamic library"),
131 ));
132 }
133 path.canonicalize()
134 .map_err(|e| UdfError::Io(display, e.to_string()))
135 }
136
137 fn expected_extension() -> &'static str {
138 if cfg!(target_os = "windows") {
139 "dll"
140 } else if cfg!(target_os = "macos") {
141 "dylib"
142 } else {
143 "so"
144 }
145 }
146}