1use std::collections::HashMap;
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6use std::time::Duration;
7
8use extism::{Manifest, Plugin, Wasm};
9
10use crate::config::{PluginConfig, PluginMetadata};
11use crate::hooks::{Hook, HookContext, HookResult};
12use crate::runtime::{BoxFuture, IsolatedContext, PluginHandle, PluginRuntime};
13use crate::sandbox::{Permission, SandboxConfig};
14use crate::types::{PluginError, PluginResult, Value};
15
16use super::isolate::WasmIsolatedContext;
17
18struct LoadedWasmPlugin {
20 name: String,
22
23 metadata: PluginMetadata,
25
26 hooks: Vec<String>,
28
29 plugin: Arc<Mutex<Plugin>>,
31}
32
33pub struct WasmRuntime {
35 plugins: HashMap<PluginHandle, LoadedWasmPlugin>,
37
38 next_handle: usize,
40
41 config: Option<PluginConfig>,
43
44 sandbox: SandboxConfig,
46
47 initialized: bool,
49}
50
51impl WasmRuntime {
52 pub fn new() -> PluginResult<Self> {
54 Ok(Self {
55 plugins: HashMap::new(),
56 next_handle: 0,
57 config: None,
58 sandbox: SandboxConfig::default(),
59 initialized: false,
60 })
61 }
62
63 fn load_plugin_with_sandbox(wasm: Wasm, sandbox: &SandboxConfig) -> Result<Plugin, String> {
70 let allow_wasi = sandbox.has_permission(Permission::Execute)
71 || sandbox.has_permission(Permission::Network);
72
73 let mut manifest = Manifest::new([wasm]);
74
75 if sandbox.max_memory > 0 {
76 let pages = (sandbox.max_memory / (64 * 1024)).max(1) as u32;
77 manifest = manifest.with_memory_max(pages);
78 }
79
80 if sandbox.timeout_ms > 0 {
81 manifest = manifest.with_timeout(Duration::from_millis(sandbox.timeout_ms));
82 }
83
84 for path in &sandbox.allowed_read_paths {
85 if let Some(s) = path.to_str() {
86 manifest = manifest.with_allowed_path(s.to_string(), path);
87 }
88 }
89
90 for path in &sandbox.allowed_write_paths {
91 if let Some(s) = path.to_str()
92 && !sandbox.allowed_read_paths.contains(path)
93 {
94 manifest = manifest.with_allowed_path(s.to_string(), path);
95 }
96 }
97
98 Plugin::new(&manifest, [], allow_wasi).map_err(|e| e.to_string())
99 }
100}
101
102impl Default for WasmRuntime {
103 fn default() -> Self {
104 Self::new().expect("Failed to create WASM runtime")
105 }
106}
107
108impl PluginRuntime for WasmRuntime {
109 fn name(&self) -> &'static str {
110 "wasm"
111 }
112
113 fn file_extensions(&self) -> &'static [&'static str] {
114 &[".wasm"]
115 }
116
117 fn init(&mut self, config: &PluginConfig) -> PluginResult<()> {
118 if self.initialized {
119 return Ok(());
120 }
121
122 self.sandbox = SandboxConfig {
124 timeout_ms: config.default_timeout_ms,
125 max_memory: config.max_memory_mb * 1024 * 1024,
126 allow_network: config.allow_network,
127 ..SandboxConfig::default()
128 };
129
130 self.config = Some(config.clone());
131 self.initialized = true;
132
133 Ok(())
134 }
135
136 fn load_plugin(&mut self, id: &str, source: &Path) -> PluginResult<PluginHandle> {
137 let wasm = Wasm::file(source);
138
139 let plugin = Self::load_plugin_with_sandbox(wasm, &self.sandbox).map_err(|e| {
140 PluginError::LoadError {
141 name: id.to_string(),
142 message: e,
143 }
144 })?;
145
146 let mut hooks = vec![];
148 for hook_name in [
149 "on_navigate",
150 "on_drill_down",
151 "on_back",
152 "on_scan_start",
153 "on_scan_progress",
154 "on_scan_complete",
155 "on_delete_start",
156 "on_delete_complete",
157 "on_copy_start",
158 "on_copy_complete",
159 "on_move_start",
160 "on_move_complete",
161 "on_render",
162 "on_action",
163 "on_mode_change",
164 "on_startup",
165 "on_shutdown",
166 ] {
167 if plugin.function_exists(hook_name) {
168 hooks.push(hook_name.to_string());
169 }
170 }
171
172 let handle = PluginHandle::new(self.next_handle);
173 self.next_handle += 1;
174
175 let metadata = PluginMetadata {
176 name: id.to_string(),
177 runtime: "wasm".to_string(),
178 ..Default::default()
179 };
180
181 self.plugins.insert(
182 handle,
183 LoadedWasmPlugin {
184 name: id.to_string(),
185 metadata,
186 hooks,
187 plugin: Arc::new(Mutex::new(plugin)),
188 },
189 );
190
191 Ok(handle)
192 }
193
194 fn unload_plugin(&mut self, handle: PluginHandle) -> PluginResult<()> {
195 self.plugins.remove(&handle);
196 Ok(())
197 }
198
199 fn get_metadata(&self, handle: PluginHandle) -> Option<&PluginMetadata> {
200 self.plugins.get(&handle).map(|p| &p.metadata)
201 }
202
203 fn has_hook(&self, handle: PluginHandle, hook_name: &str) -> bool {
204 self.plugins
205 .get(&handle)
206 .map(|p| p.hooks.contains(&hook_name.to_string()))
207 .unwrap_or(false)
208 }
209
210 fn call_hook_sync(
211 &self,
212 handle: PluginHandle,
213 hook: &Hook,
214 _ctx: &HookContext,
215 ) -> PluginResult<HookResult> {
216 let plugin = self
217 .plugins
218 .get(&handle)
219 .ok_or_else(|| PluginError::NotFound {
220 path: std::path::PathBuf::new(),
221 })?;
222
223 let mut extism_plugin = plugin
224 .plugin
225 .lock()
226 .map_err(|_| PluginError::ExecutionError {
227 name: plugin.name.clone(),
228 message: "Mutex lock failed".to_string(),
229 })?;
230
231 let hook_name = hook.name();
232
233 let payload = serde_json::to_vec(hook).map_err(|e| PluginError::ExecutionError {
234 name: plugin.name.clone(),
235 message: e.to_string(),
236 })?;
237
238 let res = extism_plugin.call::<&[u8], &[u8]>(hook_name, &payload);
239
240 match res {
241 Ok(output) => {
242 if output.is_empty() {
243 return Ok(HookResult::ok());
244 }
245
246 let result: HookResult =
247 serde_json::from_slice(output).map_err(|e| PluginError::ExecutionError {
248 name: plugin.name.clone(),
249 message: format!("Failed to parse WASM output: {}", e),
250 })?;
251
252 Ok(result)
253 }
254 Err(e) => {
255 tracing::warn!(
256 plugin = %plugin.name,
257 hook = %hook_name,
258 error = %e,
259 "WASM plugin hook failed"
260 );
261 Ok(HookResult::ok())
262 }
263 }
264 }
265
266 fn call_hook_async<'a>(
267 &'a self,
268 handle: PluginHandle,
269 hook: &'a Hook,
270 ctx: &'a HookContext,
271 ) -> BoxFuture<'a, PluginResult<HookResult>> {
272 Box::pin(async move { self.call_hook_sync(handle, hook, ctx) })
274 }
275
276 fn call_method<'a>(
277 &'a self,
278 handle: PluginHandle,
279 method: &'a str,
280 args: Vec<Value>,
281 ) -> BoxFuture<'a, PluginResult<Value>> {
282 Box::pin(async move {
283 let plugin = self
284 .plugins
285 .get(&handle)
286 .ok_or_else(|| PluginError::NotFound {
287 path: std::path::PathBuf::new(),
288 })?;
289
290 let mut extism_plugin =
291 plugin
292 .plugin
293 .lock()
294 .map_err(|_| PluginError::ExecutionError {
295 name: plugin.name.clone(),
296 message: "Mutex lock failed".to_string(),
297 })?;
298
299 let payload = serde_json::to_vec(&args).map_err(|e| PluginError::ExecutionError {
300 name: plugin.name.clone(),
301 message: e.to_string(),
302 })?;
303
304 let res = extism_plugin
305 .call::<&[u8], &[u8]>(method, payload.as_slice())
306 .map_err(|e| PluginError::ExecutionError {
307 name: plugin.name.clone(),
308 message: e.to_string(),
309 })?;
310
311 if res.is_empty() {
312 return Ok(Value::Null);
313 }
314
315 let val: Value =
316 serde_json::from_slice(res).map_err(|e| PluginError::ExecutionError {
317 name: plugin.name.clone(),
318 message: format!("Failed to parse output: {}", e),
319 })?;
320
321 Ok(val)
322 })
323 }
324
325 fn create_isolated_context(
326 &self,
327 sandbox: &SandboxConfig,
328 ) -> PluginResult<Box<dyn IsolatedContext>> {
329 Ok(Box::new(WasmIsolatedContext::new(sandbox.clone())?))
330 }
331
332 fn loaded_plugins(&self) -> Vec<PluginHandle> {
333 self.plugins.keys().copied().collect()
334 }
335
336 fn shutdown(&mut self) -> PluginResult<()> {
337 self.plugins.clear();
338 Ok(())
339 }
340}