extism_manifest/
lib.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4#[deprecated]
5pub type ManifestMemory = MemoryOptions;
6
7/// Configure memory settings
8#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
9#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
10#[serde(deny_unknown_fields)]
11pub struct MemoryOptions {
12    /// The max number of WebAssembly pages that should be allocated
13    pub max_pages: Option<u32>,
14
15    /// The maximum number of bytes allowed in an HTTP response
16    #[serde(default)]
17    pub max_http_response_bytes: Option<u64>,
18
19    /// The maximum number of bytes allowed to be used by plugin vars. Setting this to 0
20    /// will disable Extism vars. The default value is 1mb.
21    #[serde(default = "default_var_bytes")]
22    pub max_var_bytes: Option<u64>,
23}
24
25impl MemoryOptions {
26    /// Create an empty `MemoryOptions` value
27    pub fn new() -> Self {
28        Default::default()
29    }
30
31    /// Set max pages
32    pub fn with_max_pages(mut self, pages: u32) -> Self {
33        self.max_pages = Some(pages);
34        self
35    }
36
37    /// Set max HTTP response size
38    pub fn with_max_http_response_bytes(mut self, bytes: u64) -> Self {
39        self.max_http_response_bytes = Some(bytes);
40        self
41    }
42
43    /// Set max size of Extism vars
44    pub fn with_max_var_bytes(mut self, bytes: u64) -> Self {
45        self.max_var_bytes = Some(bytes);
46        self
47    }
48}
49
50fn default_var_bytes() -> Option<u64> {
51    Some(1024 * 1024)
52}
53
54/// Generic HTTP request structure
55#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
56#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
57#[serde(deny_unknown_fields)]
58pub struct HttpRequest {
59    /// The request URL
60    pub url: String,
61
62    /// Request headers
63    #[serde(default)]
64    pub headers: std::collections::BTreeMap<String, String>,
65
66    /// Request method
67    pub method: Option<String>,
68}
69
70impl HttpRequest {
71    /// Create a new `HttpRequest` to the given URL
72    pub fn new(url: impl Into<String>) -> HttpRequest {
73        HttpRequest {
74            url: url.into(),
75            headers: Default::default(),
76            method: None,
77        }
78    }
79
80    /// Update the method
81    pub fn with_method(mut self, method: impl Into<String>) -> HttpRequest {
82        self.method = Some(method.into());
83        self
84    }
85
86    /// Add a header
87    pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> HttpRequest {
88        self.headers.insert(key.into(), value.into());
89        self
90    }
91}
92
93/// Provides additional metadata about a Webassembly module
94#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
95#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
96#[serde(deny_unknown_fields)]
97pub struct WasmMetadata {
98    /// Module name, this is used by Extism to determine which is the `main` module
99    pub name: Option<String>,
100
101    /// Module hash, if the data loaded from disk or via HTTP doesn't match an error will be raised
102    pub hash: Option<String>,
103}
104
105impl From<HttpRequest> for Wasm {
106    fn from(req: HttpRequest) -> Self {
107        Wasm::Url {
108            req,
109            meta: WasmMetadata::default(),
110        }
111    }
112}
113
114impl From<std::path::PathBuf> for Wasm {
115    fn from(path: std::path::PathBuf) -> Self {
116        Wasm::File {
117            path,
118            meta: WasmMetadata::default(),
119        }
120    }
121}
122
123impl From<Vec<u8>> for Wasm {
124    fn from(data: Vec<u8>) -> Self {
125        Wasm::Data {
126            data,
127            meta: WasmMetadata::default(),
128        }
129    }
130}
131
132#[deprecated]
133pub type ManifestWasm = Wasm;
134
135/// The `Wasm` type specifies how to access a WebAssembly module
136#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
137#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
138#[serde(untagged)]
139#[serde(deny_unknown_fields)]
140pub enum Wasm {
141    /// From disk
142    File {
143        path: PathBuf,
144        #[serde(flatten)]
145        meta: WasmMetadata,
146    },
147
148    /// From memory
149    Data {
150        #[serde(with = "wasmdata")]
151        #[cfg_attr(feature = "json_schema", schemars(schema_with = "wasmdata_schema"))]
152        data: Vec<u8>,
153        #[serde(flatten)]
154        meta: WasmMetadata,
155    },
156
157    /// Via HTTP
158    Url {
159        #[serde(flatten)]
160        req: HttpRequest,
161        #[serde(flatten)]
162        meta: WasmMetadata,
163    },
164}
165
166impl Wasm {
167    /// Load Wasm from a path
168    pub fn file(path: impl AsRef<std::path::Path>) -> Self {
169        Wasm::File {
170            path: path.as_ref().to_path_buf(),
171            meta: Default::default(),
172        }
173    }
174
175    /// Load Wasm directly from a buffer
176    pub fn data(data: impl Into<Vec<u8>>) -> Self {
177        Wasm::Data {
178            data: data.into(),
179            meta: Default::default(),
180        }
181    }
182
183    /// Load Wasm from a URL
184    pub fn url(url: impl Into<String>) -> Self {
185        Wasm::Url {
186            req: HttpRequest {
187                url: url.into(),
188                headers: Default::default(),
189                method: None,
190            },
191            meta: Default::default(),
192        }
193    }
194
195    /// Load Wasm from an HTTP request
196    pub fn http(req: impl Into<HttpRequest>) -> Self {
197        Wasm::Url {
198            req: req.into(),
199            meta: Default::default(),
200        }
201    }
202
203    /// Get the metadata
204    pub fn meta(&self) -> &WasmMetadata {
205        match self {
206            Wasm::File { path: _, meta } => meta,
207            Wasm::Data { data: _, meta } => meta,
208            Wasm::Url { req: _, meta } => meta,
209        }
210    }
211
212    /// Get mutable access to the metadata
213    pub fn meta_mut(&mut self) -> &mut WasmMetadata {
214        match self {
215            Wasm::File { path: _, meta } => meta,
216            Wasm::Data { data: _, meta } => meta,
217            Wasm::Url { req: _, meta } => meta,
218        }
219    }
220
221    /// Update Wasm module name
222    pub fn with_name(mut self, name: impl Into<String>) -> Self {
223        self.meta_mut().name = Some(name.into());
224        self
225    }
226
227    /// Update Wasm module hash
228    pub fn with_hash(mut self, hash: impl Into<String>) -> Self {
229        self.meta_mut().hash = Some(hash.into());
230        self
231    }
232}
233
234#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
235#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
236#[serde(deny_unknown_fields)]
237struct DataPtrLength {
238    ptr: u64,
239    len: u64,
240}
241
242#[cfg(feature = "json_schema")]
243fn wasmdata_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
244    use schemars::{schema::SchemaObject, JsonSchema};
245    let mut schema: SchemaObject = <String>::json_schema(gen).into();
246    let objschema: SchemaObject = <DataPtrLength>::json_schema(gen).into();
247    let types = schemars::schema::SingleOrVec::<schemars::schema::InstanceType>::Vec(vec![
248        schemars::schema::InstanceType::String,
249        schemars::schema::InstanceType::Object,
250    ]);
251    schema.instance_type = Some(types);
252    schema.object = objschema.object;
253    schema.into()
254}
255
256/// The `Manifest` type is used to configure the runtime and specify how to load modules.
257#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
258#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
259#[serde(deny_unknown_fields)]
260pub struct Manifest {
261    /// WebAssembly modules, the `main` module should be named `main` or listed last
262    #[serde(default)]
263    pub wasm: Vec<Wasm>,
264
265    /// Memory options
266    #[serde(default)]
267    pub memory: MemoryOptions,
268
269    /// Config values are made accessible using the PDK `extism_config_get` function
270    #[serde(default)]
271    pub config: BTreeMap<String, String>,
272
273    #[serde(default)]
274    /// Specifies which hosts may be accessed via HTTP, if this is empty then
275    /// no hosts may be accessed. Wildcards may be used.
276    pub allowed_hosts: Option<Vec<String>>,
277
278    /// Specifies which paths should be made available on disk when using WASI. This is a mapping from
279    /// the path on disk to the path it should be available inside the plugin.
280    /// For example, `".": "/tmp"` would mount the current directory as `/tmp` inside the module
281    #[serde(default)]
282    pub allowed_paths: Option<BTreeMap<String, PathBuf>>,
283
284    /// The plugin timeout in milliseconds
285    #[serde(default)]
286    pub timeout_ms: Option<u64>,
287}
288
289impl Manifest {
290    /// Create a new manifest
291    pub fn new(wasm: impl IntoIterator<Item = impl Into<Wasm>>) -> Manifest {
292        Manifest {
293            wasm: wasm.into_iter().map(|x| x.into()).collect(),
294            ..Default::default()
295        }
296    }
297
298    pub fn with_wasm(mut self, wasm: impl Into<Wasm>) -> Self {
299        self.wasm.push(wasm.into());
300        self
301    }
302
303    /// Disallow HTTP requests to all hosts
304    pub fn disallow_all_hosts(mut self) -> Self {
305        self.allowed_hosts = Some(vec![]);
306        self
307    }
308
309    /// Set memory options
310    pub fn with_memory_options(mut self, memory: MemoryOptions) -> Self {
311        self.memory = memory;
312        self
313    }
314
315    /// Set MemoryOptions::memory_max
316    pub fn with_memory_max(mut self, max: u32) -> Self {
317        self.memory.max_pages = Some(max);
318        self
319    }
320
321    /// Add a hostname to `allowed_hosts`
322    pub fn with_allowed_host(mut self, host: impl Into<String>) -> Self {
323        match &mut self.allowed_hosts {
324            Some(h) => {
325                h.push(host.into());
326            }
327            None => self.allowed_hosts = Some(vec![host.into()]),
328        }
329
330        self
331    }
332
333    /// Set `allowed_hosts`
334    pub fn with_allowed_hosts(mut self, hosts: impl Iterator<Item = String>) -> Self {
335        self.allowed_hosts = Some(hosts.collect());
336        self
337    }
338
339    /// Add a path to `allowed_paths`
340    pub fn with_allowed_path(mut self, src: String, dest: impl AsRef<Path>) -> Self {
341        let dest = dest.as_ref().to_path_buf();
342        match &mut self.allowed_paths {
343            Some(p) => {
344                p.insert(src, dest);
345            }
346            None => {
347                let mut p = BTreeMap::new();
348                p.insert(src, dest);
349                self.allowed_paths = Some(p);
350            }
351        }
352
353        self
354    }
355
356    /// Set `allowed_paths`
357    pub fn with_allowed_paths(mut self, paths: impl Iterator<Item = (String, PathBuf)>) -> Self {
358        self.allowed_paths = Some(paths.collect());
359        self
360    }
361
362    /// Set `config`
363    pub fn with_config(
364        mut self,
365        c: impl Iterator<Item = (impl Into<String>, impl Into<String>)>,
366    ) -> Self {
367        for (k, v) in c {
368            self.config.insert(k.into(), v.into());
369        }
370        self
371    }
372
373    /// Set a single `config` key
374    pub fn with_config_key(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
375        self.config.insert(k.into(), v.into());
376        self
377    }
378
379    /// Set `timeout_ms`, which will interrupt a plugin function's execution if it meets or
380    /// exceeds this value. When an interrupt is made, the plugin will not be able to recover and
381    /// continue execution.
382    pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
383        self.timeout_ms = Some(timeout.as_millis() as u64);
384        self
385    }
386}
387
388mod wasmdata {
389    use crate::DataPtrLength;
390    use base64::{engine::general_purpose, Engine as _};
391    use serde::{Deserialize, Serialize};
392    use serde::{Deserializer, Serializer};
393    use std::slice;
394
395    pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
396        let base64 = general_purpose::STANDARD.encode(v.as_slice());
397        String::serialize(&base64, s)
398    }
399
400    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
401        #[derive(Deserialize)]
402        #[serde(untagged)]
403        enum WasmDataTypes {
404            String(String),
405            DataPtrLength(DataPtrLength),
406        }
407        Ok(match WasmDataTypes::deserialize(d)? {
408            WasmDataTypes::String(string) => general_purpose::STANDARD
409                .decode(string.as_bytes())
410                .map_err(serde::de::Error::custom)?,
411            WasmDataTypes::DataPtrLength(ptrlen) => {
412                let slice =
413                    unsafe { slice::from_raw_parts(ptrlen.ptr as *const u8, ptrlen.len as usize) };
414                slice.to_vec()
415            }
416        })
417    }
418}
419
420impl From<Manifest> for std::borrow::Cow<'_, [u8]> {
421    fn from(m: Manifest) -> Self {
422        let s = serde_json::to_vec(&m).unwrap();
423        std::borrow::Cow::Owned(s)
424    }
425}
426
427impl From<&Manifest> for std::borrow::Cow<'_, [u8]> {
428    fn from(m: &Manifest) -> Self {
429        let s = serde_json::to_vec(&m).unwrap();
430        std::borrow::Cow::Owned(s)
431    }
432}