extism_runtime/
manifest.rs

1use std::collections::BTreeMap;
2use std::fmt::Write as FmtWrite;
3use std::io::Read;
4
5use sha2::Digest;
6
7use crate::*;
8
9/// Manifest wraps the manifest exported by `extism_manifest`
10#[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
65/// Convert from manifest to a wasmtime Module
66fn 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            // Figure out a good name for the file
74            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            // Load file
83            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            // Get the file name
109            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                // Setup request
141                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                // Fetch WASM code
148                let mut r = req.call()?.into_reader();
149                let mut data = Vec::new();
150                r.read_to_end(&mut data)?;
151
152                // Try to cache file
153                if let Some(hash) = &meta.hash {
154                    cache_add_file(hash, &data);
155                }
156
157                check_hash(&meta.hash, &data)?;
158
159                // Convert fetched data to module
160                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    /// Create a new Manifest, returns the manifest and a map of modules
171    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 there's only one module, it should be called `main`
204        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}