javy_codegen/
plugin.rs

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