1use anyhow::{anyhow, bail, Result};
2use std::{borrow::Cow, fs, path::Path, str};
3use walrus::{ExportItem, ValType};
4use wasmparser::Parser;
5
6#[derive(Debug, Default, PartialEq, Copy, Clone)]
9#[allow(dead_code)] pub(crate) enum PluginKind {
11 #[default]
12 User,
13 V2,
14}
15
16impl PluginKind {
17 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 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()); 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#[derive(Clone, Debug, Default)]
60pub struct Plugin {
61 bytes: Cow<'static, [u8]>,
62}
63
64impl Plugin {
65 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 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 pub fn as_bytes(&self) -> &[u8] {
86 &self.bytes
87 }
88
89 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}