1use std::time::Duration;
29
30use wasmtime::component::{Component, Linker, ResourceTable};
31use wasmtime::{Engine, Store};
32use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
33
34use crate::generated::yosh::plugin::types::{ErrorCode, HookName, IoStream};
35use crate::generated::{PluginWorld, PluginWorldPre};
36
37pub struct MetadataCtx {
42 table: ResourceTable,
43 wasi: WasiCtx,
44}
45
46impl Default for MetadataCtx {
47 fn default() -> Self {
48 let wasi = WasiCtxBuilder::new().build();
53 MetadataCtx {
54 table: ResourceTable::new(),
55 wasi,
56 }
57 }
58}
59
60impl WasiView for MetadataCtx {
61 fn ctx(&mut self) -> &mut WasiCtx {
62 &mut self.wasi
63 }
64
65 fn table(&mut self) -> &mut ResourceTable {
66 &mut self.table
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct ExtractedMetadata {
73 pub name: String,
75 pub version: String,
76 pub commands: Vec<String>,
77 pub required_capabilities: Vec<String>,
78 pub implemented_hooks: Vec<String>,
79}
80
81pub fn extract(engine: &Engine, wasm_bytes: &[u8]) -> Result<ExtractedMetadata, String> {
86 let component = Component::new(engine, wasm_bytes)
87 .map_err(|e| format!("metadata: compile component: {}", e))?;
88
89 let mut linker = Linker::<MetadataCtx>::new(engine);
90 register_limited_wasi(&mut linker).map_err(|e| format!("metadata: register WASI: {}", e))?;
91 register_all_deny_imports(&mut linker)
92 .map_err(|e| format!("metadata: register deny stubs: {}", e))?;
93
94 let pre = PluginWorldPre::new(
95 linker
96 .instantiate_pre(&component)
97 .map_err(|e| format!("metadata: instantiate_pre: {}", e))?,
98 )
99 .map_err(|e| format!("metadata: bindings pre-init: {}", e))?;
100
101 let mut store = Store::new(engine, MetadataCtx::default());
102 store.set_epoch_deadline(1);
104
105 let watchdog_engine: Engine = engine.clone();
109 let _watchdog = std::thread::Builder::new()
110 .name("yosh-plugin-metadata-watchdog".to_string())
111 .spawn(move || {
112 std::thread::sleep(Duration::from_secs(5));
113 watchdog_engine.increment_epoch();
116 });
117
118 let plugin_world: PluginWorld = pre
119 .instantiate(&mut store)
120 .map_err(|e| format!("metadata: instantiate: {}", e))?;
121
122 let info = plugin_world
123 .yosh_plugin_plugin()
124 .call_metadata(&mut store)
125 .map_err(|e| format!("metadata: call: {}", e))?;
126
127 Ok(ExtractedMetadata {
128 name: info.name,
129 version: info.version,
130 commands: info.commands,
131 required_capabilities: info.required_capabilities,
132 implemented_hooks: info
133 .implemented_hooks
134 .into_iter()
135 .map(hook_name_to_string)
136 .collect(),
137 })
138}
139
140fn hook_name_to_string(h: HookName) -> String {
141 match h {
142 HookName::PreExec => "pre-exec".into(),
143 HookName::PostExec => "post-exec".into(),
144 HookName::OnCd => "on-cd".into(),
145 HookName::PrePrompt => "pre-prompt".into(),
146 }
147}
148
149fn register_limited_wasi(linker: &mut Linker<MetadataCtx>) -> wasmtime::Result<()> {
154 use wasmtime_wasi::WasiImpl;
155 use wasmtime_wasi::bindings::{clocks, random};
156
157 let closure = type_annotate::<MetadataCtx, _>(|t| WasiImpl(t));
158 clocks::wall_clock::add_to_linker_get_host(linker, closure)?;
159 clocks::monotonic_clock::add_to_linker_get_host(linker, closure)?;
160 random::random::add_to_linker_get_host(linker, closure)?;
161 Ok(())
162}
163
164fn type_annotate<T, F>(val: F) -> F
167where
168 F: Fn(&mut T) -> wasmtime_wasi::WasiImpl<&mut T>,
169{
170 val
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 Ok(())
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn metadata_ctx_default_constructs() {
228 let _c = MetadataCtx::default();
229 }
230
231 #[test]
232 fn linker_registration_smoke() {
233 let engine = crate::precompile::make_engine().unwrap();
234 let mut linker = Linker::<MetadataCtx>::new(&engine);
235 register_limited_wasi(&mut linker).expect("limited wasi");
236 register_all_deny_imports(&mut linker).expect("deny stubs");
237 }
238}