1use std::time::Duration;
38
39use wasmtime::component::{Component, Linker, ResourceTable};
40use wasmtime::{Engine, Store};
41use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
42
43use crate::generated::yosh::plugin::commands::ExecOutput;
44use crate::generated::yosh::plugin::files::{DirEntry, FileStat};
45use crate::generated::yosh::plugin::types::{ErrorCode, HookName, IoStream};
46use crate::generated::{PluginWorld, PluginWorldPre};
47
48pub struct MetadataCtx {
53 table: ResourceTable,
54 wasi: WasiCtx,
55}
56
57impl Default for MetadataCtx {
58 fn default() -> Self {
59 let wasi = WasiCtxBuilder::new().build();
67 MetadataCtx {
68 table: ResourceTable::new(),
69 wasi,
70 }
71 }
72}
73
74impl WasiView for MetadataCtx {
75 fn ctx(&mut self) -> &mut WasiCtx {
76 &mut self.wasi
77 }
78
79 fn table(&mut self) -> &mut ResourceTable {
80 &mut self.table
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct ExtractedMetadata {
87 pub name: String,
89 pub version: String,
90 pub commands: Vec<String>,
91 pub required_capabilities: Vec<String>,
92 pub implemented_hooks: Vec<String>,
93}
94
95pub fn extract(engine: &Engine, wasm_bytes: &[u8]) -> Result<ExtractedMetadata, String> {
100 let component = Component::new(engine, wasm_bytes)
101 .map_err(|e| format!("metadata: compile component: {}", e))?;
102
103 let mut linker = Linker::<MetadataCtx>::new(engine);
104 register_wasi(&mut linker).map_err(|e| format!("metadata: register WASI: {}", e))?;
105 register_all_deny_imports(&mut linker)
106 .map_err(|e| format!("metadata: register deny stubs: {}", e))?;
107
108 let pre = PluginWorldPre::new(
109 linker
110 .instantiate_pre(&component)
111 .map_err(|e| format!("metadata: instantiate_pre: {}", e))?,
112 )
113 .map_err(|e| format!("metadata: bindings pre-init: {}", e))?;
114
115 let mut store = Store::new(engine, MetadataCtx::default());
116 store.set_epoch_deadline(1);
118
119 let watchdog_engine: Engine = engine.clone();
123 let _watchdog = std::thread::Builder::new()
124 .name("yosh-plugin-metadata-watchdog".to_string())
125 .spawn(move || {
126 std::thread::sleep(Duration::from_secs(5));
127 watchdog_engine.increment_epoch();
130 });
131
132 let plugin_world: PluginWorld = pre
133 .instantiate(&mut store)
134 .map_err(|e| format!("metadata: instantiate: {}", e))?;
135
136 let info = plugin_world
137 .yosh_plugin_plugin()
138 .call_metadata(&mut store)
139 .map_err(|e| format!("metadata: call: {}", e))?;
140
141 Ok(ExtractedMetadata {
142 name: info.name,
143 version: info.version,
144 commands: info.commands,
145 required_capabilities: info.required_capabilities,
146 implemented_hooks: info
147 .implemented_hooks
148 .into_iter()
149 .map(hook_name_to_string)
150 .collect(),
151 })
152}
153
154fn hook_name_to_string(h: HookName) -> String {
155 match h {
156 HookName::PreExec => "pre-exec".into(),
157 HookName::PostExec => "post-exec".into(),
158 HookName::OnCd => "on-cd".into(),
159 HookName::PrePrompt => "pre-prompt".into(),
160 }
161}
162
163fn register_wasi(linker: &mut Linker<MetadataCtx>) -> wasmtime::Result<()> {
170 wasmtime_wasi::add_to_linker_sync(linker)
171}
172
173fn register_all_deny_imports(linker: &mut Linker<MetadataCtx>) -> wasmtime::Result<()> {
178 let mut vars = linker.instance("yosh:plugin/variables@0.2.1")?;
179 vars.func_wrap(
180 "get",
181 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_,): (String,)| {
182 Ok::<_, wasmtime::Error>((Err::<Option<String>, ErrorCode>(ErrorCode::Denied),))
183 },
184 )?;
185 vars.func_wrap(
186 "set",
187 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (String, String)| {
188 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
189 },
190 )?;
191 vars.func_wrap(
192 "export-env",
193 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (String, String)| {
194 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
195 },
196 )?;
197
198 let mut fs = linker.instance("yosh:plugin/filesystem@0.2.1")?;
199 fs.func_wrap(
200 "cwd",
201 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (): ()| {
202 Ok::<_, wasmtime::Error>((Err::<String, ErrorCode>(ErrorCode::Denied),))
203 },
204 )?;
205 fs.func_wrap(
206 "set-cwd",
207 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_,): (String,)| {
208 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
209 },
210 )?;
211
212 let mut io = linker.instance("yosh:plugin/io@0.2.1")?;
213 io.func_wrap(
214 "write",
215 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (IoStream, Vec<u8>)| {
216 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
217 },
218 )?;
219
220 let mut files = linker.instance("yosh:plugin/files@0.2.1")?;
221 files.func_wrap(
222 "read-file",
223 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_,): (String,)| {
224 Ok::<_, wasmtime::Error>((Err::<Vec<u8>, ErrorCode>(ErrorCode::Denied),))
225 },
226 )?;
227 files.func_wrap(
228 "read-dir",
229 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_,): (String,)| {
230 Ok::<_, wasmtime::Error>((Err::<Vec<DirEntry>, ErrorCode>(ErrorCode::Denied),))
231 },
232 )?;
233 files.func_wrap(
234 "metadata",
235 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_,): (String,)| {
236 Ok::<_, wasmtime::Error>((Err::<FileStat, ErrorCode>(ErrorCode::Denied),))
237 },
238 )?;
239 files.func_wrap(
240 "write-file",
241 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (String, Vec<u8>)| {
242 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
243 },
244 )?;
245 files.func_wrap(
246 "append-file",
247 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (String, Vec<u8>)| {
248 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
249 },
250 )?;
251 files.func_wrap(
252 "create-dir",
253 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (String, bool)| {
254 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
255 },
256 )?;
257 files.func_wrap(
258 "remove-file",
259 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_,): (String,)| {
260 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
261 },
262 )?;
263 files.func_wrap(
264 "remove-dir",
265 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (String, bool)| {
266 Ok::<_, wasmtime::Error>((Err::<(), ErrorCode>(ErrorCode::Denied),))
267 },
268 )?;
269
270 let mut commands = linker.instance("yosh:plugin/commands@0.2.1")?;
271 commands.func_wrap(
272 "exec",
273 |_store: wasmtime::StoreContextMut<'_, MetadataCtx>, (_, _): (String, Vec<String>)| {
274 Ok::<_, wasmtime::Error>((Err::<ExecOutput, ErrorCode>(ErrorCode::Denied),))
275 },
276 )?;
277 Ok(())
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn metadata_ctx_default_constructs() {
286 let _c = MetadataCtx::default();
287 }
288
289 #[test]
290 fn linker_registration_smoke() {
291 let engine = crate::precompile::make_engine().unwrap();
292 let mut linker = Linker::<MetadataCtx>::new(&engine);
293 register_wasi(&mut linker).expect("wasi");
294 register_all_deny_imports(&mut linker).expect("deny stubs");
295 }
296}