Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ serde_json = { workspace = true }
test-r = { workspace = true }
tokio = { workspace = true }
tokio-util = { workspace = true }
uuid = { version = "1.18.1", features = ["v4"] }
wac-graph = { workspace = true }
wasmtime = { workspace = true, features = ["async", "component-model"] }
wasmtime-wasi = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,10 @@ Implemented by https://github.com/MattiasBuelens/web-streams-polyfill
- `read`
- `write`

### Crypto
- `crypto.randomUUID`
- `crypto.getRandomValues`

### Coming from QuickJS

- Global:
Expand Down
2 changes: 2 additions & 0 deletions crates/wasm-rquickjs/skeleton/Cargo.toml_
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ futures = { version = "0.3.31", features = [] }
futures-concurrency = "7.6.3"
pin-project = "1.1.10"
url = "2.5.7"
uuid = { version = "1.18.1", features = ["v4"] }
rand = "0.9.2"
wasi = "0.12.1+wasi-0.2.0"
wasi-async-runtime = "0.1.2"
wit-bindgen-rt = { version = "0.42.1", features = ["bitflags"] }
Expand Down
16 changes: 14 additions & 2 deletions crates/wasm-rquickjs/skeleton/src/builtin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod streams;
mod timeout;
mod url;
mod util;
mod web_crypto;

pub fn add_module_resolvers(
resolver: rquickjs::loader::BuiltinResolver,
Expand Down Expand Up @@ -52,6 +53,8 @@ pub fn add_module_resolvers(
.with_module("process")
.with_module("__wasm_rquickjs_builtin/url_native")
.with_module("__wasm_rquickjs_builtin/url")
.with_module("__wasm_rquickjs_builtin/web_crypto_native")
.with_module("__wasm_rquickjs_builtin/web_crypto")
}

pub fn module_loader() -> (
Expand Down Expand Up @@ -81,7 +84,11 @@ pub fn module_loader() -> (
"__wasm_rquickjs_builtin/process_native",
process::js_native_module,
)
.with_module("__wasm_rquickjs_builtin/url_native", url::js_native_module),
.with_module("__wasm_rquickjs_builtin/url_native", url::js_native_module)
.with_module(
"__wasm_rquickjs_builtin/web_crypto_native",
web_crypto::js_native_module,
),
rquickjs::loader::BuiltinLoader::default()
.with_module("__wasm_rquickjs_builtin/console", console::CONSOLE_JS)
.with_module("__wasm_rquickjs_builtin/timeout", timeout::TIMEOUT_JS)
Expand All @@ -100,7 +107,11 @@ pub fn module_loader() -> (
.with_module("fs", fs::FS_JS)
.with_module("node:process", process::PROCESS_JS)
.with_module("process", process::PROCESS_JS)
.with_module("__wasm_rquickjs_builtin/url", url::URL_JS),
.with_module("__wasm_rquickjs_builtin/url", url::URL_JS)
.with_module(
"__wasm_rquickjs_builtin/web_crypto",
web_crypto::WEB_CRYPTO_JS,
),
)
}

Expand All @@ -112,6 +123,7 @@ pub fn wire_builtins() -> String {
writeln!(result, "{}", streams::WIRE_JS).unwrap();
writeln!(result, "{}", encoding::WIRE_JS).unwrap();
writeln!(result, "{}", url::WIRE_JS).unwrap();
writeln!(result, "{}", web_crypto::WIRE_JS).unwrap();

result
}
34 changes: 34 additions & 0 deletions crates/wasm-rquickjs/skeleton/src/builtin/web-crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as webCryptoNative from '__wasm_rquickjs_builtin/web_crypto_native'

export function getRandomValues(typedArray) {
if (typedArray instanceof Int8Array) {
webCryptoNative.randomize_int8_array(typedArray);
} else if (typedArray instanceof Uint8Array) {
webCryptoNative.randomize_uint8_array(typedArray);
} else if (typedArray instanceof Uint8ClampedArray) {
webCryptoNative.randomize_uint8_clamped_array(typedArray);
} else if (typedArray instanceof Int16Array) {
webCryptoNative.randomize_int16_array(typedArray);
} else if (typedArray instanceof Uint16Array) {
webCryptoNative.randomize_uint16_array(typedArray);
} else if (typedArray instanceof Int32Array) {
webCryptoNative.randomize_int32_array(typedArray);
} else if (typedArray instanceof Uint32Array) {
webCryptoNative.randomize_uint32_array(typedArray);
} else if (typedArray instanceof BigInt64Array) {
webCryptoNative.randomize_bigint64_array(typedArray);
} else if (typedArray instanceof BigUint64Array) {
webCryptoNative.randomize_biguint64_array(typedArray);
} else {
throw new TypeError('Unsupported TypedArray type');
}
return typedArray;
}

/**
* Generate a random UUID
* @returns A string containing a randomly generated, 36 character long v4 UUID.
*/
export function randomUUID() {
return webCryptoNative.random_uuid_v4_string();
}
76 changes: 76 additions & 0 deletions crates/wasm-rquickjs/skeleton/src/builtin/web_crypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use rand::RngCore;
use rquickjs::TypedArray;
use std::slice;

// Native functions for the crypto implementation
#[rquickjs::module(rename = "camelCase")]
pub mod native_module {
use rquickjs::TypedArray;

#[rquickjs::function]
pub fn random_uuid_v4_string() -> String {
let uuid = uuid::Uuid::new_v4();
uuid.to_string()
}

#[rquickjs::function]
pub fn randomize_int8_array<'js>(array: TypedArray<'js, i8>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_uint8_array<'js>(array: TypedArray<'js, u8>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_uint8_clamped_array<'js>(array: TypedArray<'js, u8>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_int16_array<'js>(array: TypedArray<'js, i16>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_uint16_array<'js>(array: TypedArray<'js, u16>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_int32_array<'js>(array: TypedArray<'js, i32>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_uint32_array<'js>(array: TypedArray<'js, u32>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_bigint64_array<'js>(array: TypedArray<'js, i64>) {
super::randomize_typed_array(array);
}

#[rquickjs::function]
pub fn randomize_biguint64_array<'js>(array: TypedArray<'js, u64>) {
super::randomize_typed_array(array);
}
}

fn randomize_typed_array<V>(array: TypedArray<V>) {
if let Some(raw) = array.as_raw() {
let slice = unsafe { slice::from_raw_parts_mut(raw.ptr.as_ptr(), raw.len) };
rand::rng().fill_bytes(slice);
}
}

// JS functions for the crypto implementation
pub const WEB_CRYPTO_JS: &str = include_str!("web-crypto.js");

// JS code wiring the crypto module into the global context
pub const WIRE_JS: &str = r#"
import * as __wasm_rquickjs_web_crypto from '__wasm_rquickjs_builtin/web_crypto';
globalThis.crypto = __wasm_rquickjs_web_crypto;
"#;
4 changes: 4 additions & 0 deletions crates/wasm-rquickjs/skeleton/src/wrappers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const RESULT_ERR: &str = "err";

/// rquickjs supports passing tuples as arguments but only up to 8 elements. This wrapper
/// provides support up to 26 elements.
#[allow(dead_code)]
pub struct JsArgs<T>(pub T);

macro_rules! impl_into_args {
Expand Down Expand Up @@ -76,6 +77,7 @@ impl_into_args!(
/// The Result is encoded in an object with two fields:
/// - `tag`: a string that is either "ok" or "err", indicating the result type.
/// - `val`: the value of the result, which can be either the success value or the error value.
#[allow(dead_code)]
pub struct JsResult<Ok, Err>(pub Result<Ok, Err>);

impl<'js, Ok: IntoJs<'js>, Err: IntoJs<'js>> IntoJs<'js> for JsResult<Ok, Err> {
Expand Down Expand Up @@ -118,6 +120,7 @@ impl<'js, Ok: FromJs<'js>, Err: FromJs<'js>> FromJs<'js> for JsResult<Ok, Err> {
}

// Wrapper type that forces the js type to be a bigint instead of the default number which can loose some bits due to
#[allow(dead_code)]
pub struct BigIntWrapper<T>(pub T);

impl<'js> IntoJs<'js> for BigIntWrapper<u64> {
Expand Down Expand Up @@ -150,6 +153,7 @@ impl<'js> FromJs<'js> for BigIntWrapper<i64> {
}
}

#[allow(dead_code)]
pub struct UInt8Array(pub Vec<u8>);

impl<'js> IntoJs<'js> for UInt8Array {
Expand Down
18 changes: 18 additions & 0 deletions examples/crypto/src/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

export function newUuids() {
const uuid1 = crypto.randomUUID()
const uuid2 = crypto.randomUUID()
return [uuid1, uuid2]
}

export function randomS8(count) {
const buf = new Int8Array(count);
crypto.getRandomValues(buf);
return Array.from(buf);
}

export function randomU32(count) {
const buf = new Uint32Array(count);
crypto.getRandomValues(buf);
return Array.from(buf);
}
7 changes: 7 additions & 0 deletions examples/crypto/wit/crypto.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package quickjs:crypto;

world crypto {
export new-uuids: func() -> tuple<string, string>;
export random-s8: func(count: u32) -> list<s8>;
export random-u32: func(count: u32) -> list<u32>;
}
5 changes: 5 additions & 0 deletions tests/goldenfiles/generated_types_crypto_exports.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module 'crypto' {
export function newUuids(): Promise<[string, string]>;
export function randomS8(count: number): Promise<number[]>;
export function randomU32(count: number): Promise<number[]>;
}
100 changes: 100 additions & 0 deletions tests/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ fn compiled_url() -> CompiledTest {
CompiledTest::new(path, false).expect("Failed to compile url")
}

#[test_dep(tagged_as = "crypto")]
fn compiled_crypto() -> CompiledTest {
let path = Utf8Path::new("examples/crypto");
CompiledTest::new(path, false).expect("Failed to compile crypto")
}

#[test]
async fn example1_sync(#[tagged_as("example1")] compiled: &CompiledTest) -> anyhow::Result<()> {
let (result, output) = invoke_and_capture_output(
Expand Down Expand Up @@ -1102,3 +1108,97 @@ async fn url_test4(#[tagged_as("url")] compiled_test: &CompiledTest) -> anyhow::
);
Ok(())
}

#[test]
async fn web_crypto_random_uuid(
#[tagged_as("crypto")] compiled: &CompiledTest,
) -> anyhow::Result<()> {
let (result, _output) =
invoke_and_capture_output(compiled.wasm_path(), None, "new-uuids", &[]).await;

let result = result?;

match result {
Some(Val::Tuple(vals)) => {
let strings = vals
.into_iter()
.filter_map(|v| match v {
Val::String(s) => Some(s),
_ => None,
})
.collect::<Vec<_>>();

assert_eq!(strings.len(), 2);

strings
.into_iter()
.map(|s| uuid::Uuid::parse_str(&s))
.collect::<Result<Vec<_>, _>>()?;

Ok(())
}
_ => Err(anyhow!("Expected tuple result")),
}
}

#[test]
async fn web_crypto_random_s8(
#[tagged_as("crypto")] compiled: &CompiledTest,
) -> anyhow::Result<()> {
let (result, _output) =
invoke_and_capture_output(compiled.wasm_path(), None, "random-s8", &[Val::U32(10)]).await;

let result = result?;

match result {
Some(Val::List(vals)) => {
let bytes = vals
.into_iter()
.filter_map(|v| match v {
Val::S8(b) => Some(b),
_ => None,
})
.collect::<Vec<_>>();

assert_eq!(bytes.len(), 10);
assert!(
bytes.iter().any(|b| b != &bytes[0]),
"There should be some different bytes in the list"
);

Ok(())
}
_ => Err(anyhow!("Expected list<s8> result")),
}
}

#[test]
async fn web_crypto_random_u32(
#[tagged_as("crypto")] compiled: &CompiledTest,
) -> anyhow::Result<()> {
let (result, _output) =
invoke_and_capture_output(compiled.wasm_path(), None, "random-u32", &[Val::U32(10)]).await;

let result = result?;

match result {
Some(Val::List(vals)) => {
let numbers = vals
.into_iter()
.filter_map(|v| match v {
Val::U32(n) => Some(n),
_ => None,
})
.collect::<Vec<_>>();

assert_eq!(numbers.len(), 10);
assert!(
numbers.iter().any(|b| b != &numbers[0]),
"There should be some different numbers in the list"
);

Ok(())
}
_ => Err(anyhow!("Expected list<u32> result")),
}
}
Loading