1use anyhow::{Result, anyhow, bail};
2use std::{borrow::Cow, fs, path::Path, str};
3use walrus::{ExportItem, ValType};
4use wasmparser::Parser;
5
6#[derive(Clone, Debug, Default)]
8pub struct Plugin {
9 bytes: Cow<'static, [u8]>,
10}
11
12impl Plugin {
13 pub fn new(bytes: Cow<'static, [u8]>) -> Result<Self> {
15 Self::validate(&bytes)?;
16 Ok(Self { bytes })
17 }
18
19 pub fn new_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
21 let bytes = fs::read(path)?;
22 Self::new(bytes.into())
23 }
24
25 pub fn as_bytes(&self) -> &[u8] {
27 &self.bytes
28 }
29
30 pub fn validate(plugin_bytes: &[u8]) -> Result<()> {
32 if !Parser::is_core_wasm(plugin_bytes) {
33 bail!("Could not process plugin: Expected Wasm module, received unknown file type");
34 }
35
36 let mut errors = vec![];
37
38 let module = walrus::Module::from_buffer(plugin_bytes)?;
39
40 if module.exports.get_func("compile_src").is_ok() {
41 bail!("Could not process plugin: Using unsupported legacy plugin API");
42 }
43
44 if let Err(err) = validate_exported_func(&module, "initialize-runtime", &[], &[]) {
45 errors.push(err);
46 }
47 if let Err(err) = validate_exported_func(
48 &module,
49 "compile-src",
50 &[ValType::I32, ValType::I32],
51 &[ValType::I32],
52 ) {
53 errors.push(err);
54 }
55 if let Err(err) = validate_exported_func(
56 &module,
57 "invoke",
58 &[
59 ValType::I32,
60 ValType::I32,
61 ValType::I32,
62 ValType::I32,
63 ValType::I32,
64 ],
65 &[],
66 ) {
67 errors.push(err);
68 }
69
70 let has_memory = module
71 .exports
72 .iter()
73 .any(|export| export.name == "memory" && matches!(export.item, ExportItem::Memory(_)));
74 if !has_memory {
75 errors.push("missing exported memory named `memory`".to_string());
76 }
77
78 let has_import_namespace = module
79 .customs
80 .iter()
81 .any(|(_, section)| section.name() == "import_namespace");
82 if !has_import_namespace {
83 errors.push("missing custom section named `import_namespace`".to_string());
84 }
85
86 if !errors.is_empty() {
87 bail!("Could not process plugin: {}", errors.join(", "))
88 }
89 Ok(())
90 }
91
92 pub(crate) fn import_namespace(&self) -> Result<String> {
93 let module = walrus::Module::from_buffer(&self.bytes)?;
94 let import_namespace: std::borrow::Cow<'_, [u8]> = module
95 .customs
96 .iter()
97 .find_map(|(_, section)| {
98 if section.name() == "import_namespace" {
99 Some(section)
100 } else {
101 None
102 }
103 })
104 .ok_or_else(|| anyhow!("Plugin is missing import_namespace custom section"))?
105 .data(&Default::default()); Ok(str::from_utf8(&import_namespace)?.to_string())
107 }
108}
109
110fn validate_exported_func(
111 module: &walrus::Module,
112 name: &str,
113 expected_params: &[ValType],
114 expected_results: &[ValType],
115) -> Result<(), String> {
116 let func_id = module
117 .exports
118 .get_func(name)
119 .map_err(|_| format!("missing export for function named `{name}`"))?;
120 let function = module.funcs.get(func_id);
121 let ty_id = function.ty();
122 let ty = module.types.get(ty_id);
123 let params = ty.params();
124 let has_correct_params = params == expected_params;
125 let results = ty.results();
126 let has_correct_results = results == expected_results;
127 if !has_correct_params || !has_correct_results {
128 return Err(format!("type for function `{name}` is incorrect"));
129 }
130
131 Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136 use anyhow::Result;
137 use walrus::{FunctionBuilder, ModuleConfig, ValType};
138
139 use crate::Plugin;
140
141 #[test]
142 fn test_validate_plugin_with_empty_file() -> Result<()> {
143 let err = Plugin::new(vec![].into()).err().unwrap();
144 assert_eq!(
145 err.to_string(),
146 "Could not process plugin: Expected Wasm module, received unknown file type"
147 );
148 Ok(())
149 }
150
151 #[test]
152 fn test_validate_plugin_with_old_plugin() -> Result<()> {
153 let mut module = walrus::Module::with_config(ModuleConfig::default());
154 module.add_import_memory("foo", "memory", false, false, 0, None, None);
155 let mut compile_src_fn = FunctionBuilder::new(
156 &mut module.types,
157 &[ValType::I32, ValType::I32],
158 &[ValType::I32],
159 );
160 compile_src_fn.func_body().unreachable();
161 let compile_src_fn = compile_src_fn.finish(vec![], &mut module.funcs);
162 module.exports.add("compile_src", compile_src_fn);
163
164 let err = Plugin::new(module.emit_wasm().into()).err().unwrap();
165 assert_eq!(
166 err.to_string(),
167 "Could not process plugin: Using unsupported legacy plugin API"
168 );
169 Ok(())
170 }
171
172 #[test]
173 fn test_validate_plugin_with_incorrect_invoke_and_everything_missing() -> Result<()> {
174 let mut module = walrus::Module::with_config(ModuleConfig::default());
175 let invoke = FunctionBuilder::new(
176 &mut module.types,
177 &[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
178 &[],
179 )
180 .finish(vec![], &mut module.funcs);
181 module.exports.add("invoke", invoke);
182
183 let plugin_bytes = module.emit_wasm();
184 let error = Plugin::validate(&plugin_bytes).err().unwrap();
185 assert_eq!(
186 error.to_string(),
187 "Could not process plugin: missing export for function named \
188 `initialize-runtime`, missing export for function named \
189 `compile-src`, type for function `invoke` is incorrect, missing \
190 exported memory named `memory`, missing custom section named \
191 `import_namespace`"
192 );
193 Ok(())
194 }
195
196 #[test]
197 fn test_validate_plugin_with_everything_missing() -> Result<()> {
198 let mut empty_module = walrus::Module::with_config(ModuleConfig::default());
199 let plugin_bytes = empty_module.emit_wasm();
200 let error = Plugin::new(plugin_bytes.into()).err().unwrap();
201 assert_eq!(
202 error.to_string(),
203 "Could not process plugin: missing export for function named \
204 `initialize-runtime`, missing export for function named \
205 `compile-src`, missing export for function named `invoke`, \
206 missing exported memory named `memory`, missing custom section \
207 named `import_namespace`"
208 );
209 Ok(())
210 }
211
212 #[test]
213 fn test_validate_plugin_with_wrong_params_for_initialize_runtime() -> Result<()> {
214 let mut module = walrus::Module::with_config(ModuleConfig::default());
215 let initialize_runtime = FunctionBuilder::new(&mut module.types, &[ValType::I32], &[])
216 .finish(vec![], &mut module.funcs);
217 module.exports.add("initialize-runtime", initialize_runtime);
218
219 let plugin_bytes = module.emit_wasm();
220 let error = Plugin::new(plugin_bytes.into()).err().unwrap();
221 let expected_part_of_error =
222 "Could not process plugin: type for function `initialize-runtime` is incorrect,";
223 if !error.to_string().contains(expected_part_of_error) {
224 panic!(
225 "Expected error to contain '{expected_part_of_error}' but it did not. Full error is: '{error}'"
226 );
227 }
228 Ok(())
229 }
230
231 #[test]
232 fn test_validate_plugin_with_wrong_results_for_initialize_runtime() -> Result<()> {
233 let mut module = walrus::Module::with_config(ModuleConfig::default());
234 let mut initialize_runtime = FunctionBuilder::new(&mut module.types, &[], &[ValType::I32]);
235 initialize_runtime.func_body().i32_const(0);
236 let initialize_runtime = initialize_runtime.finish(vec![], &mut module.funcs);
237 module.exports.add("initialize-runtime", initialize_runtime);
238
239 let plugin_bytes = module.emit_wasm();
240 let error = Plugin::new(plugin_bytes.into()).err().unwrap();
241 let expected_part_of_error =
242 "Could not process plugin: type for function `initialize-runtime` is incorrect,";
243 if !error.to_string().contains(expected_part_of_error) {
244 panic!(
245 "Expected error to contain '{expected_part_of_error}' but it did not. Full error is: '{error}'"
246 );
247 }
248 Ok(())
249 }
250}