diff --git a/Cargo.lock b/Cargo.lock index a80281e2..fed2bf6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -438,6 +438,12 @@ dependencies = [ "cc", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation" version = "0.9.3" @@ -1334,6 +1340,7 @@ dependencies = [ "anyhow", "binaryen", "brotli", + "convert_case", "criterion", "lazy_static", "num-format", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a4b3c3ec..f62dac31 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -27,6 +27,7 @@ wasi-common = { workspace = true } walrus = "0.20.1" swc_core = { version = "0.78.15", features = ["common_sourcemap", "ecma_ast", "ecma_parser"] } wit-parser = "0.8.0" +convert_case = "0.4.0" [dev-dependencies] serde_json = "1.0" diff --git a/crates/cli/src/exports.rs b/crates/cli/src/exports.rs new file mode 100644 index 00000000..d33ed006 --- /dev/null +++ b/crates/cli/src/exports.rs @@ -0,0 +1,28 @@ +use anyhow::{anyhow, Result}; +use convert_case::{Case, Casing}; +use std::path::Path; + +use crate::{js::JS, wit}; + +pub struct Export { + pub wit: String, + pub js: String, +} + +pub fn process_exports(js: &JS, wit: &Path, wit_world: &str) -> Result> { + let js_exports = js.exports()?; + wit::parse_exports(wit, wit_world)? + .into_iter() + .map(|wit_export| { + let export = wit_export.from_case(Case::Kebab).to_case(Case::Camel); + if !js_exports.contains(&export) { + Err(anyhow!("JS module does not export {export}")) + } else { + Ok(Export { + wit: wit_export, + js: export, + }) + } + }) + .collect::>>() +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 7b6a06b9..1abbedf2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,12 +1,14 @@ mod bytecode; mod commands; +mod exports; mod js; mod wasm_generator; mod wit; use crate::commands::{Command, CompileCommandOpts, EmitProviderCommandOpts}; use crate::wasm_generator::r#static as static_generator; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; +use exports::Export; use js::JS; use std::env; use std::fs::File; @@ -23,7 +25,12 @@ fn main() -> Result<()> { Command::EmitProvider(opts) => emit_provider(opts), Command::Compile(opts) => { let js = JS::from_file(&opts.input)?; - let exports = determine_js_exports(&js, opts)?; + let exports = match (&opts.wit, &opts.wit_world) { + (None, None) => Ok(vec![]), + (None, Some(_)) => Ok(vec![]), + (Some(_), None) => bail!("Must provide WIT world when providing WIT file"), + (Some(wit), Some(world)) => exports::process_exports(&js, wit, world), + }?; if opts.dynamic { let wasm = dynamic_generator::generate(&js, exports)?; fs::write(&opts.output, wasm)?; @@ -44,7 +51,7 @@ fn emit_provider(opts: &EmitProviderCommandOpts) -> Result<()> { Ok(()) } -fn create_statically_linked_module(opts: &CompileCommandOpts, exports: Vec) -> Result<()> { +fn create_statically_linked_module(opts: &CompileCommandOpts, exports: Vec) -> Result<()> { // The javy-core `main.rs` pre-initializer uses WASI to read the JS source // code from stdin. Wizer doesn't let us customize its WASI context so we // don't have a better option right now. Since we can't set the content of @@ -83,22 +90,3 @@ fn create_statically_linked_module(opts: &CompileCommandOpts, exports: Vec Result> { - let js_exports = js.exports()?; - match (&opts.wit, &opts.wit_world) { - (None, None) => Ok(vec![]), - (None, Some(_)) => Ok(vec![]), - (Some(_), None) => bail!("Must provide WIT world when providing WIT file"), - (Some(path), Some(world)) => wit::parse_exports(path, world)? - .into_iter() - .map(|wit_export| { - if !js_exports.contains(&wit_export) { - Err(anyhow!("JS module does not export {wit_export}")) - } else { - Ok(wit_export) - } - }) - .collect::>>(), - } -} diff --git a/crates/cli/src/wasm_generator/dynamic.rs b/crates/cli/src/wasm_generator/dynamic.rs index 9426ca6d..3934852c 100644 --- a/crates/cli/src/wasm_generator/dynamic.rs +++ b/crates/cli/src/wasm_generator/dynamic.rs @@ -1,4 +1,4 @@ -use crate::js::JS; +use crate::{exports::Export, js::JS}; use super::transform::{self, SourceCodeSection}; use anyhow::Result; @@ -64,7 +64,7 @@ use walrus::{DataKind, FunctionBuilder, Module, ValType}; // (data (;0;) "\02\05\18function.mjs\06foo\0econsole\06log\06bar\0f\bc\03\00\01\00\00\be\03\00\00\0e\00\06\01\a0\01\00\00\00\03\01\01\1a\00\be\03\00\01\08\ea\05\c0\00\e1)8\e0\00\00\00B\e1\00\00\00\04\e2\00\00\00$\01\00)\bc\03\01\04\01\00\07\0a\0eC\06\01\be\03\00\00\00\03\00\00\13\008\e0\00\00\00B\e1\00\00\00\04\df\00\00\00$\01\00)\bc\03\01\02\03]") // (data (;1;) "foo") // ) -pub fn generate(js: &JS, exported_functions: Vec) -> Result> { +pub fn generate(js: &JS, exported_functions: Vec) -> Result> { let mut module = Module::with_config(transform::module_config()); const IMPORT_NAMESPACE: &str = "javy_quickjs_provider_v1"; @@ -123,9 +123,9 @@ pub fn generate(js: &JS, exported_functions: Vec) -> Result> { let (invoke_fn, _) = module.add_import_func(IMPORT_NAMESPACE, "invoke", invoke_type); let fn_name_ptr_local = module.locals.add(ValType::I32); - for js_export in exported_functions { + for export in exported_functions { // For each JS function export, add an export that copies the name of the function into memory and invokes it. - let js_export_bytes = js_export.as_bytes(); + let js_export_bytes = export.js.as_bytes(); let js_export_len: i32 = js_export_bytes.len().try_into().unwrap(); let fn_name_data = module.data.add(DataKind::Passive, js_export_bytes.to_vec()); @@ -161,7 +161,7 @@ pub fn generate(js: &JS, exported_functions: Vec) -> Result> { .i32_const(js_export_len) .call(invoke_fn); let export_fn = export_fn.finish(vec![], &mut module.funcs); - module.exports.add(&js_export, export_fn); + module.exports.add(&export.wit, export_fn); } } diff --git a/crates/cli/src/wasm_generator/static.rs b/crates/cli/src/wasm_generator/static.rs index 5c59f485..fd508146 100644 --- a/crates/cli/src/wasm_generator/static.rs +++ b/crates/cli/src/wasm_generator/static.rs @@ -5,7 +5,7 @@ use binaryen::{CodegenConfig, Module}; use walrus::{DataKind, ExportItem, FunctionBuilder, FunctionId, MemoryId, ValType}; use wizer::Wizer; -use crate::js::JS; +use crate::{exports::Export, js::JS}; use super::transform::{self, SourceCodeSection}; @@ -26,7 +26,7 @@ pub fn generate() -> Result> { /// Takes Wasm created by `Generator` and makes additional changes. /// /// This is intended to be run in the parent process after generating the Wasm. -pub fn refine(wasm: Vec, js: &JS, exports: Vec) -> Result> { +pub fn refine(wasm: Vec, js: &JS, exports: Vec) -> Result> { let mut module = transform::module_config().parse(&wasm)?; let (realloc, invoke, memory) = { @@ -82,12 +82,12 @@ fn export_exported_js_functions( realloc_fn: FunctionId, invoke_fn: FunctionId, memory: MemoryId, - js_exports: Vec, + js_exports: Vec, ) { let ptr_local = module.locals.add(ValType::I32); - for js_export in js_exports { + for export in js_exports { // For each JS function export, add an export that copies the name of the function into memory and invokes it. - let js_export_bytes = js_export.as_bytes(); + let js_export_bytes = export.js.as_bytes(); let js_export_len: i32 = js_export_bytes.len().try_into().unwrap(); let fn_name_data = module.data.add(DataKind::Passive, js_export_bytes.to_vec()); @@ -108,6 +108,6 @@ fn export_exported_js_functions( .i32_const(js_export_len) .call(invoke_fn); let export_fn = export_fn.finish(vec![], &mut module.funcs); - module.exports.add(&js_export, export_fn); + module.exports.add(&export.wit, export_fn); } } diff --git a/crates/cli/tests/dynamic_linking_test.rs b/crates/cli/tests/dynamic_linking_test.rs index 7ef7f715..5fdc656a 100644 --- a/crates/cli/tests/dynamic_linking_test.rs +++ b/crates/cli/tests/dynamic_linking_test.rs @@ -20,15 +20,15 @@ pub fn test_dynamic_linking() -> Result<()> { #[test] pub fn test_dynamic_linking_with_func() -> Result<()> { - let js_src = "export function foo() { console.log('In foo'); }; console.log('Toplevel');"; + let js_src = "export function fooBar() { console.log('In foo'); }; console.log('Toplevel');"; let wit = " package local:main world foo-test { - export foo: func() + export foo-bar: func() } "; - let log_output = invoke_fn_on_generated_module(js_src, "foo", Some((wit, "foo-test")))?; + let log_output = invoke_fn_on_generated_module(js_src, "foo-bar", Some((wit, "foo-test")))?; assert_eq!("Toplevel\nIn foo\n", &log_output); Ok(()) } diff --git a/crates/cli/tests/integration_test.rs b/crates/cli/tests/integration_test.rs index 3c379def..82238fd5 100644 --- a/crates/cli/tests/integration_test.rs +++ b/crates/cli/tests/integration_test.rs @@ -107,6 +107,8 @@ fn test_exported_functions() { let (_, logs, fuel_consumed) = run_fn(&mut runner, "foo", &[]); assert_eq!("Hello from top-level\nHello from foo\n", logs); assert_fuel_consumed_within_threshold(54610, fuel_consumed); + let (_, logs, _) = run_fn(&mut runner, "foo-bar", &[]); + assert_eq!("Hello from top-level\nHello from fooBar\n", logs); } #[cfg(feature = "experimental_event_loop")] diff --git a/crates/cli/tests/sample-scripts/exported-fn.js b/crates/cli/tests/sample-scripts/exported-fn.js index 9110c9c5..d7d86a44 100644 --- a/crates/cli/tests/sample-scripts/exported-fn.js +++ b/crates/cli/tests/sample-scripts/exported-fn.js @@ -6,4 +6,8 @@ export function foo() { console.log("Hello from foo"); } +export function fooBar() { + console.log("Hello from fooBar"); +} + console.log("Hello from top-level"); diff --git a/crates/cli/tests/sample-scripts/exported-fn.wit b/crates/cli/tests/sample-scripts/exported-fn.wit index aca505ce..4672a6dd 100644 --- a/crates/cli/tests/sample-scripts/exported-fn.wit +++ b/crates/cli/tests/sample-scripts/exported-fn.wit @@ -3,4 +3,5 @@ package local:test world exported-fn { export foo: func() export bar: func() + export foo-bar: func() } diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index be88a4d8..6e3e18bf 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -1130,6 +1130,12 @@ who = "Johan Andersson " criteria = "safe-to-deploy" version = "1.0.58" +[[audits.embark-studios.audits.convert_case]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = "No unsafe usage or ambient capabilities" + [[audits.embark-studios.audits.epaint]] who = "Johan Andersson " criteria = "safe-to-deploy"