extism_runtime/
manifest.rs1use std::collections::BTreeMap;
2use std::fmt::Write as FmtWrite;
3use std::io::Read;
4
5use sha2::Digest;
6
7use crate::*;
8
9#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
11#[serde(transparent)]
12pub struct Manifest(extism_manifest::Manifest);
13
14fn hex(data: &[u8]) -> String {
15 let mut s = String::new();
16 for &byte in data {
17 write!(&mut s, "{:02x}", byte).unwrap();
18 }
19 s
20}
21
22#[allow(unused)]
23fn cache_add_file(hash: &str, data: &[u8]) -> Result<(), Error> {
24 let cache_dir = std::env::temp_dir().join("exitsm-cache");
25 let _ = std::fs::create_dir(&cache_dir);
26 let file = cache_dir.join(hash);
27 if file.exists() {
28 return Ok(());
29 }
30 std::fs::write(file, data)?;
31 Ok(())
32}
33
34fn cache_get_file(hash: &str) -> Result<Option<Vec<u8>>, Error> {
35 let cache_dir = std::env::temp_dir().join("exitsm-cache");
36 let file = cache_dir.join(hash);
37 if file.exists() {
38 let r = std::fs::read(file)?;
39 return Ok(Some(r));
40 }
41
42 Ok(None)
43}
44
45fn check_hash(hash: &Option<String>, data: &[u8]) -> Result<(), Error> {
46 match hash {
47 None => Ok(()),
48 Some(hash) => {
49 let digest = sha2::Sha256::digest(data);
50 let hex = hex(&digest);
51 if &hex != hash {
52 return Err(anyhow::format_err!(
53 "Hash mismatch, found {} but expected {}",
54 hex,
55 hash
56 ));
57 }
58 Ok(())
59 }
60 }
61}
62
63const WASM: &[u8] = include_bytes!("extism-runtime.wasm");
64
65fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, Module), Error> {
67 match wasm {
68 extism_manifest::Wasm::File { path, meta } => {
69 if cfg!(not(feature = "register-filesystem")) {
70 return Err(anyhow::format_err!("File-based registration is disabled"));
71 }
72
73 let name = match &meta.name {
75 None => {
76 let name = path.with_extension("");
77 name.file_name().unwrap().to_string_lossy().to_string()
78 }
79 Some(n) => n.clone(),
80 };
81
82 let mut buf = Vec::new();
84 let mut file = std::fs::File::open(path)?;
85 file.read_to_end(&mut buf)?;
86
87 check_hash(&meta.hash, &buf)?;
88
89 Ok((name, Module::new(engine, buf)?))
90 }
91 extism_manifest::Wasm::Data { meta, data } => {
92 check_hash(&meta.hash, data)?;
93 Ok((
94 meta.name.as_deref().unwrap_or("main").to_string(),
95 Module::new(engine, data)?,
96 ))
97 }
98 #[allow(unused)]
99 extism_manifest::Wasm::Url {
100 req:
101 extism_manifest::HttpRequest {
102 url,
103 headers,
104 method,
105 },
106 meta,
107 } => {
108 let file_name = url.split('/').last().unwrap_or_default();
110 let name = match &meta.name {
111 Some(name) => name.as_str(),
112 None => {
113 let mut name = "main";
114 if let Some(n) = file_name.strip_suffix(".wasm") {
115 name = n;
116 }
117
118 if let Some(n) = file_name.strip_suffix(".wast") {
119 name = n;
120 }
121 name
122 }
123 };
124
125 if let Some(h) = &meta.hash {
126 if let Ok(Some(data)) = cache_get_file(h) {
127 check_hash(&meta.hash, &data)?;
128 let module = Module::new(engine, data)?;
129 return Ok((name.to_string(), module));
130 }
131 }
132
133 #[cfg(not(feature = "register-http"))]
134 {
135 return Err(anyhow::format_err!("HTTP registration is disabled"));
136 }
137
138 #[cfg(feature = "register-http")]
139 {
140 let mut req = ureq::request(method.as_deref().unwrap_or("GET"), url);
142
143 for (k, v) in headers.iter() {
144 req = req.set(k, v);
145 }
146
147 let mut r = req.call()?.into_reader();
149 let mut data = Vec::new();
150 r.read_to_end(&mut data)?;
151
152 if let Some(hash) = &meta.hash {
154 cache_add_file(hash, &data);
155 }
156
157 check_hash(&meta.hash, &data)?;
158
159 let module = Module::new(engine, data)?;
161 Ok((name.to_string(), module))
162 }
163 }
164 }
165}
166
167const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d];
168
169impl Manifest {
170 pub fn new(engine: &Engine, data: &[u8]) -> Result<(Self, BTreeMap<String, Module>), Error> {
172 let extism_module = Module::new(engine, WASM)?;
173 let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
174 let is_wast = data.starts_with(b"(module") || data.starts_with(b";;");
175 if !has_magic && !is_wast {
176 if let Ok(s) = std::str::from_utf8(data) {
177 if let Ok(t) = toml::from_str::<Self>(s) {
178 let m = t.modules(engine)?;
179 return Ok((t, m));
180 }
181 }
182
183 let t = serde_json::from_slice::<Self>(data)?;
184 let mut m = t.modules(engine)?;
185 m.insert("env".to_string(), extism_module);
186 return Ok((t, m));
187 }
188
189 let m = Module::new(engine, data)?;
190 let mut modules = BTreeMap::new();
191 modules.insert("env".to_string(), extism_module);
192 modules.insert("main".to_string(), m);
193 Ok((Manifest::default(), modules))
194 }
195
196 fn modules(&self, engine: &Engine) -> Result<BTreeMap<String, Module>, Error> {
197 if self.0.wasm.is_empty() {
198 return Err(anyhow::format_err!("No wasm files specified"));
199 }
200
201 let mut modules = BTreeMap::new();
202
203 if self.0.wasm.len() == 1 {
205 let (_, m) = to_module(engine, &self.0.wasm[0])?;
206 modules.insert("main".to_string(), m);
207 return Ok(modules);
208 }
209
210 for f in &self.0.wasm {
211 let (name, m) = to_module(engine, f)?;
212 modules.insert(name, m);
213 }
214
215 Ok(modules)
216 }
217}
218
219impl AsRef<extism_manifest::Manifest> for Manifest {
220 fn as_ref(&self) -> &extism_manifest::Manifest {
221 &self.0
222 }
223}
224
225impl AsMut<extism_manifest::Manifest> for Manifest {
226 fn as_mut(&mut self) -> &mut extism_manifest::Manifest {
227 &mut self.0
228 }
229}