javy_codegen/
plugin.rs

1use anyhow::{Result, anyhow, bail};
2use std::{borrow::Cow, fs, path::Path, str};
3use walrus::{ExportItem, ValType};
4use wasmparser::Parser;
5
6/// A Javy plugin.
7#[derive(Clone, Debug, Default)]
8pub struct Plugin {
9    bytes: Cow<'static, [u8]>,
10}
11
12impl Plugin {
13    /// Constructs a new [`Plugin`].
14    pub fn new(bytes: Cow<'static, [u8]>) -> Result<Self> {
15        Self::validate(&bytes)?;
16        Ok(Self { bytes })
17    }
18
19    /// Constructs a new [`Plugin`] from a given path.
20    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    /// Returns the [`Plugin`] as bytes
26    pub fn as_bytes(&self) -> &[u8] {
27        &self.bytes
28    }
29
30    /// Validates if `plugin_bytes` are a valid plugin.
31    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()); // Argument is required but not actually used for anything.
106        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}