diff --git a/.github/workflows/host-checks.yml b/.github/workflows/host-checks.yml index 5e6e2be..8b64b78 100644 --- a/.github/workflows/host-checks.yml +++ b/.github/workflows/host-checks.yml @@ -60,6 +60,9 @@ jobs: - name: Cargo clippy (warnings are errors) run: cargo clippy --all-targets --locked -- -D warnings + - name: Cargo clippy with Wasm host tools + run: cargo clippy --bin hyperlight-unikraft --tests --locked --features wasm-host-fns -- -D warnings + - name: Enable KVM permissions working-directory: . run: | @@ -75,6 +78,11 @@ jobs: RUST_BACKTRACE: '1' run: cargo test --all-targets --locked + - name: Cargo test with Wasm host tools + env: + RUST_BACKTRACE: '1' + run: cargo test --bin hyperlight-unikraft --locked --features wasm-host-fns wasm_host_fns::tests + host-checks-passed: if: always() needs: [checks] diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index b5a743a..d372ee3 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -7,10 +7,10 @@ on: push: branches: [main] paths: - - 'examples/**' - - 'runtimes/**' - - 'host/**' - - '.github/workflows/test-examples.yml' + - "examples/**" + - "runtimes/**" + - "host/**" + - ".github/workflows/test-examples.yml" # Pushing a new commit to the same ref cancels any in-flight run on # the previous commit — don't waste CI on stale code. @@ -68,7 +68,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.25.1' + go-version: "1.25.1" cache: false - name: Install just @@ -92,7 +92,7 @@ jobs: - name: Build local-python-base images (python-agent-driver only) if: matrix.example == 'python-agent-driver' env: - DOCKER_BUILDKIT: '0' + DOCKER_BUILDKIT: "0" run: | docker build --target base -t local-python-base-dev:latest \ -f runtimes/python.Dockerfile runtimes/ @@ -102,7 +102,7 @@ jobs: - name: Build rootfs working-directory: examples/${{ matrix.example }} env: - DOCKER_BUILDKIT: '0' + DOCKER_BUILDKIT: "0" run: | just rootfs @@ -185,8 +185,9 @@ jobs: args: "-- /hello.py" expect: "Hello from Python on Hyperlight" - example: python-tools - args: "--enable-tools -- /test_tools.py" + args: "-- /test_tools.py" expect: "Tool returned" + needs_echo_tool: true - example: nodejs args: "-- /app/hello.js" expect: "Hello from Node.js on Hyperlight" @@ -280,13 +281,19 @@ jobs: if: steps.kvm_check.outputs.available == 'true' run: | cd host - cargo build --release --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run + cargo build --release --features wasm-host-fns --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run sudo cp target/release/hyperlight-unikraft /usr/local/bin/ + - name: Build echo Wasm host function + if: steps.kvm_check.outputs.available == 'true' && matrix.needs_echo_tool == true + run: | + rustup target add wasm32-wasip1 + cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1 + - name: Build local-python-base images (python-agent-driver only) if: steps.kvm_check.outputs.available == 'true' && matrix.example == 'python-agent-driver' env: - DOCKER_BUILDKIT: '0' + DOCKER_BUILDKIT: "0" run: | docker build --target base -t local-python-base-dev:latest \ -f runtimes/python.Dockerfile runtimes/ @@ -297,7 +304,7 @@ jobs: if: steps.kvm_check.outputs.available == 'true' working-directory: examples/${{ matrix.example }} env: - DOCKER_BUILDKIT: '0' + DOCKER_BUILDKIT: "0" run: | just rootfs kraft-hyperlight --no-prompt build --plat hyperlight --arch x86_64 || true @@ -348,6 +355,12 @@ jobs: mkdir -p "$mount_dir" mount_args="--mount $mount_dir:/host" fi + + tool_args="" + if [ "${{ matrix.needs_echo_tool }}" = "true" ]; then + tool_args="--tool echo=../echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm" + fi + # HTTP server examples: start in background, poll, curl, kill. http_port="${{ matrix.http_port }}" if [ -n "$http_port" ]; then @@ -355,7 +368,7 @@ jobs: if [ -n "$memory" ]; then mem_args="-m $memory" fi - hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" ${{ matrix.args }} & + hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args $tool_args ${{ matrix.args }} & server_pid=$! sleep 3 ready=0 @@ -396,7 +409,7 @@ jobs: if [ -n "$memory" ]; then mem_args="-m $memory" fi - cmd=(timeout 120 hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args ${{ matrix.args }}) + cmd=(timeout 120 hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args $tool_args ${{ matrix.args }}) ;; esac set +e @@ -456,7 +469,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.25.1' + go-version: "1.25.1" cache: false - name: Install just @@ -480,7 +493,7 @@ jobs: - name: Build local-python-base images (python-agent-driver only) if: matrix.example == 'python-agent-driver' env: - DOCKER_BUILDKIT: '0' + DOCKER_BUILDKIT: "0" run: | docker build --target base -t local-python-base-dev:latest \ -f runtimes/python.Dockerfile runtimes/ @@ -490,7 +503,7 @@ jobs: - name: Build rootfs + kernel working-directory: examples/${{ matrix.example }} env: - DOCKER_BUILDKIT: '0' + DOCKER_BUILDKIT: "0" run: | just rootfs kraft-hyperlight --no-prompt build --plat hyperlight --arch x86_64 || true @@ -562,8 +575,9 @@ jobs: args: "-- /hello.py" expect: "Hello from Python on Hyperlight" - example: python-tools - args: "--enable-tools -- /test_tools.py" + args: "-- /test_tools.py" expect: "Tool returned" + needs_echo_tool: true - example: nodejs args: "-- /app/hello.js" expect: "Hello from Node.js on Hyperlight" @@ -631,12 +645,19 @@ jobs: shell: pwsh run: | cd host - cargo build --release --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run --bin pyhl + cargo build --release --features wasm-host-fns --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run --bin pyhl Copy-Item target\release\hyperlight-unikraft.exe $env:USERPROFILE\.cargo\bin\ -Force Copy-Item target\release\multifn-test.exe $env:USERPROFILE\.cargo\bin\ -Force Copy-Item target\release\pydriver-run.exe $env:USERPROFILE\.cargo\bin\ -Force Copy-Item target\release\pyhl.exe $env:USERPROFILE\.cargo\bin\ -Force + - name: Build echo Wasm host function + if: matrix.needs_echo_tool == true + shell: pwsh + run: | + rustup target add wasm32-wasip1 + cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1 + - name: Download prebuilt image uses: actions/download-artifact@v4 with: @@ -652,6 +673,7 @@ jobs: $driver = '${{ matrix.driver }}' $runArgs = '${{ matrix.args }}' $needsMount = '${{ matrix.needs_mount }}' + $needsEchoTool = '${{ matrix.needs_echo_tool }}' # Read memory from the example's Justfile (single source of truth). $memory = '' @@ -673,6 +695,11 @@ jobs: $mountArgs = @('--mount', ($mountDir + ':/host')) } + $toolArgs = @() + if ($needsEchoTool -eq 'true') { + $toolArgs = @('--tool', 'echo=examples/echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm') + } + # Strict mode: expected output must appear AND the driver # must exit 0. Matches Linux semantics. $PSNativeCommandUseErrorActionPreference = $false @@ -747,7 +774,7 @@ jobs: $memArgs = @('-m', $memory) } $out = & hyperlight-unikraft -q @memArgs ` - $kernel --initrd $cpio @mountArgs @argList 2>&1 + $kernel --initrd $cpio @mountArgs @toolArgs @argList 2>&1 $rc = $LASTEXITCODE } } @@ -921,7 +948,14 @@ jobs: test-examples-passed: if: always() - needs: [build-example, runtime-test, package-images-for-windows, runtime-test-windows, pyhl-snapshot-test] + needs: + [ + build-example, + runtime-test, + package-images-for-windows, + runtime-test-windows, + pyhl-snapshot-test, + ] runs-on: ubuntu-latest permissions: {} steps: diff --git a/README.md b/README.md index 2a4b0e9..824459b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ This project enables running Linux applications (Python, Node.js, Go, Rust, C/C+ ### Key Features -- **Thin, opt-in host surface** — by default, the guest has no access to the host filesystem, network, or any host functions. When you enable features like `--mount`, `--net`, or `--enable-tools`, a single `__dispatch` JSON-RPC bridge is registered as the only guest→host channel. See [HOST_FUNCTIONS.md](HOST_FUNCTIONS.md) for the full list of dispatchable operations +- **Thin, opt-in host surface** — by default, the guest has no access to the host filesystem, network, or custom host tools. When you enable capabilities like `--mount`, `--net`, or `--tool`, a single `__dispatch` JSON-RPC bridge is registered as the only guest→host channel. See [host_functions.md](docs/host_functions.md) for the full list of dispatchable operations - **Identity-mapped memory** - Simplified memory layout (vaddr == paddr) - **Generic cmdline mechanism** - Pass arguments to any application via `-- arg1 arg2 ...` - **Fast cold start** - Hyperlight's lightweight design enables millisecond startup times @@ -182,6 +182,7 @@ just run | `helloworld-c` | Static PIE C binary | Compiled with `musl-gcc` | | `rust` | Static PIE Rust binary | Compiled with `rustc --target x86_64-unknown-linux-musl` | | `python` | CPython 3.12 | Rootfs from Docker, script passed via cmdline | +| `python-tools` | CPython 3.12 + host function call | Calls an echo Wasm host function registered with `--tool echo=...` | | `go` | Static PIE Go binary | Compiled with musl via Docker for CGO support | | `nodejs` | Node.js 21 | Rootfs from Alpine, script passed via cmdline | | `hostfs-posix-c` | C + unmodified POSIX | `open`/`read`/`write`/`mkdir` against `/host`, forwarded by `lib/hostfs` | @@ -281,7 +282,18 @@ Options: -m, --memory Memory allocation (e.g., 256Mi, 512Mi, 1Gi) [default: 512Mi] --stack Stack size (e.g., 8Mi) [default: 8Mi] -q, --quiet Quiet mode — suppress host-side status messages - --enable-tools Enable tool dispatch via __dispatch host function + --tool Register a WASIp1 module as a host tool (requires wasm-host-fns) + --tool-wasi-dir + Preopen a read-write host directory for Wasm tools + --tool-wasi-dir-ro + Preopen a read-only host directory for Wasm tools + --tool-wasi-env + Set an environment variable for Wasm tools + --tool-wasi-env-inherit + Inherit one host environment variable into Wasm tools + --tool-wasi-fuel Fuel units available to each Wasm tool call [default: 100000000] + --tool-wasi-output-limit + Maximum stdout or stderr captured from one Wasm tool call [default: 1Mi] --mount Preopen a host directory for the guest's sandboxed filesystem (repeatable; default guest path: /host) --net Enable guest networking (off by default) @@ -297,6 +309,20 @@ Options: -V, --version Print version ``` +### Wasm host tools + +Build the CLI with the optional `wasm-host-fns` feature to register host-side custom tools from WASIp1 modules: + +```bash +cargo build --manifest-path host/Cargo.toml --release --features wasm-host-fns --bin hyperlight-unikraft +rustup target add wasm32-wasip1 +cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1 +hyperlight-unikraft kernel --initrd app.cpio \ + --tool echo=examples/echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm +``` + +The guest still calls the existing `__dispatch` envelope, for example `{"name":"echo","args":{"message":"hello"}}`. The Wasm handler runs on the host in Wasmtime, receives that request JSON on stdin, and writes either a raw JSON result or `{"result": ...}` / `{"error": "..."}` to stdout. WASI filesystem and environment access are off unless granted with `--tool-wasi-dir`, `--tool-wasi-dir-ro`, `--tool-wasi-env`, or `--tool-wasi-env-inherit`. + ## Project Structure ``` diff --git a/docs/host_functions.md b/docs/host_functions.md index 3c5b1e3..a4c7575 100644 --- a/docs/host_functions.md +++ b/docs/host_functions.md @@ -15,7 +15,7 @@ Implementation lives in [`host/src/lib.rs`](../host/src/lib.rs). Guest-side call | `fs_*` tools | **Off** | `--mount HOST[:GUEST]` (repeatable) | | `net_*` tools | **Off** | `--net`, `--net-allow`, or `--net-block` | | Inbound listen | **Off** | `--port PORT` (requires network enabled) | -| Custom tools | **Off** | `--enable-tools` + `SandboxBuilder::tool()` | +| Custom tools | **Off** | `--tool NAME=WASM` with `wasm-host-fns` or `SandboxBuilder::tool()` | With **no flags**, the guest cannot reach the host filesystem or network through dispatch. Only internal plumbing (`__hl_exit`, `__hl_sleep`) is wired. @@ -85,7 +85,17 @@ hyperlight-unikraft KERNEL [--initrd CPIO] [options] [-- APP_ARGS...] | `--net-allow HOST_OR_IP` | Allow-list outbound destinations (implies `--net`). Repeatable. | | `--net-block HOST_OR_IP` | Block-list; all other destinations allowed (implies `--net`). Mutually exclusive with `--net-allow`. | | `--port PORT` | Allow `net_bind` / listen on `PORT` (implies `--net`). Without `--port`, outbound-only: bind is rejected. | -| `--enable-tools` | Enables custom tool registration. Registers a built-in `echo` tool (used by the `python-tools` example). Library users add their own tools via `SandboxBuilder::tool()`. | +| `--tool NAME=WASM` | With the Cargo feature `wasm-host-fns`, registers `WASM` as a host-side WASIp1 custom tool named `NAME`. Repeatable. | +| `--tool-wasi-dir HOST[:GUEST]` | Preopens a read-write host directory for every CLI Wasm tool. Default guest path is `/host`. Repeatable. | +| `--tool-wasi-dir-ro HOST[:GUEST]` | Preopens a read-only host directory for every CLI Wasm tool. Default guest path is `/host`. Repeatable. | +| `--tool-wasi-env KEY=VALUE` | Sets an environment variable for every CLI Wasm tool. Repeatable. | +| `--tool-wasi-env-inherit KEY` | Copies one host environment variable into every CLI Wasm tool. Repeatable. | +| `--tool-wasi-fuel FUEL` | Sets the instruction-fuel budget for each call to every CLI Wasm tool. Default `100000000`. | +| `--tool-wasi-output-limit SIZE` | Caps captured stdout and stderr for each call to every CLI Wasm tool. Default `1Mi`. | + +`--tool-wasi-*` flags configure the Wasmtime/WASI sandbox for Wasm custom tools only. They do not expose the guest `--mount` filesystem, and they do not change the `fs_*` handlers used by `lib/hostfs`. + +The CLI currently applies the same Wasm filesystem, environment, fuel, and output settings to every `--tool` registered in one invocation. If tools need different permissions or limits, do not grant the union to all handlers; that requires a narrower per-tool configuration surface or a separate host integration. **Mount rules (host-enforced before boot):** @@ -189,7 +199,39 @@ Sockets are host-side (`socket2`); the guest sees opaque numeric **`fd`** handle ## Custom tools -**CLI:** `--enable-tools` registers a built-in `echo` tool (returns `args` unchanged) used by the [`python-tools` example](../examples/python-tools). The primary purpose of `--enable-tools` is to demonstrate custom host function registration via the API. +**CLI Wasm tools:** build with the optional feature and pass one or more `--tool` flags. The `examples/echo-wasm-host-fxn` module is a minimal echo handler used by `examples/python-tools`: + +```bash +cargo build --manifest-path host/Cargo.toml --features wasm-host-fns --bin hyperlight-unikraft +rustup target add wasm32-wasip1 +cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1 +hyperlight-unikraft kernel --initrd app.cpio \ + --tool echo=examples/echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm +``` + +Each `--tool NAME=WASM` module is compiled and linked before VM boot, then invoked as a fresh WASIp1 command for every matching guest `__dispatch` call. The handler receives the existing dispatch request on stdin: + +```json +{"name":"echo","args":} +``` + +The handler writes JSON to stdout. It may write either a raw JSON result value or the normal dispatch envelope: + +```json +{"result":} +``` + +```json +{"error":"message"} +``` + +A raw value is treated as the tool result. A single-key `result` envelope is unwrapped. A single-key `error` envelope becomes the outer `__dispatch` error response. Empty stdout returns JSON null. + +Wasm tools are separate from the built-in `fs_*` and `net_*` dispatch handlers. `--mount` controls what the guest can access through `lib/hostfs`; `--tool-wasi-dir*` controls what the host-side Wasm handler can access through its own WASI filesystem view. + +WASI capabilities are denied by default except stdio used for the protocol, clocks, and random. Use `--tool-wasi-dir`, `--tool-wasi-dir-ro`, `--tool-wasi-env`, and `--tool-wasi-env-inherit` to grant explicit filesystem and environment access to handlers. These grants and the `--tool-wasi-fuel` / `--tool-wasi-output-limit` settings apply to every CLI Wasm tool registered by the process. Tool names beginning with `__`, `fs_`, or `net_` are reserved. + +**Why WASIp1 command modules today?** The current CLI maps one `--tool NAME=WASM` flag to one tool name and one fresh handler invocation. WASIp1 keeps that ABI small: JSON request on stdin, JSON response on stdout, no long-lived reactor state, and broad language/toolchain support. Component-model or reactor-style handlers could support a future `--tools component.wasm` shape with multiple exported tools and auto-registration, but that would need a separate registration and lifecycle model; it is not the current ABI. **Library:** @@ -199,7 +241,7 @@ Sandbox::builder("kernel") .build()?; ``` -Custom handlers run with the same JSON request/response envelope as built-in tools. +`SandboxBuilder::tool()` handlers receive the inner `args` JSON value from the dispatch request; the registry has already matched the outer `name`. Handler return values become the `result` field in the outer `__dispatch` response, and handler errors become `{"error": "..."}`. --- @@ -214,6 +256,8 @@ Custom handlers run with the same JSON request/response envelope as built-in too | `fs_list` entries | 100 000 | | `net_send` / `net_sendto` | 1 MiB decoded bytes | | `__hl_sleep` | 60 s | +| Wasm tool fuel | 100 000 000 instructions per call by default; configurable with `--tool-wasi-fuel`; same value applies to every CLI Wasm tool | +| Wasm tool stdout / stderr | 1 MiB each per call by default; configurable with `--tool-wasi-output-limit`; same value applies to every CLI Wasm tool | | Open host sockets | 1024 per sandbox | | AllowList learned DNS IPs | 256 | @@ -242,6 +286,14 @@ Custom handlers run with the same JSON request/response envelope as built-in too - A compromised guest can invoke any **registered** tool name; do not register powerful custom tools unless needed. - Payload size is capped; malformed JSON fails closed with an error response. +**When `--tool` is used with `wasm-host-fns`:** + +- Handler code runs on the host inside Wasmtime, not inside the Unikraft VM. +- WASI filesystem and environment access are capability-based and off unless explicitly granted with `--tool-wasi-*` flags. +- CLI Wasm capability and limit flags apply to every registered Wasm tool; avoid combining handlers with different privilege needs in one invocation. +- Fuel limits bound Wasm instruction execution, but do not turn filesystem operations into a full wall-clock timeout. +- Handlers are untrusted code from the host operator's filesystem; only load modules you intend to grant these capabilities to. + **Not exposed via dispatch:** Host shell, arbitrary process spawn, unrestricted host `exec`, or kernel modules — only the tools listed above. **Operators should:** Use minimal flags, allow-lists over `--net` where possible, mount least-privilege directories, and run guests with the smallest initrd/runtime required. diff --git a/examples/echo-wasm-host-fxn/Cargo.lock b/examples/echo-wasm-host-fxn/Cargo.lock new file mode 100644 index 0000000..90e7768 --- /dev/null +++ b/examples/echo-wasm-host-fxn/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "echo-wasm-host-fxn" +version = "0.1.0" diff --git a/examples/echo-wasm-host-fxn/Cargo.toml b/examples/echo-wasm-host-fxn/Cargo.toml new file mode 100644 index 0000000..0844a4c --- /dev/null +++ b/examples/echo-wasm-host-fxn/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "echo-wasm-host-fxn" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "echo-wasm-host-fxn" +path = "src/main.rs" diff --git a/examples/echo-wasm-host-fxn/Justfile b/examples/echo-wasm-host-fxn/Justfile new file mode 100644 index 0000000..eb87a1e --- /dev/null +++ b/examples/echo-wasm-host-fxn/Justfile @@ -0,0 +1,19 @@ +# echo-wasm-host-fxn +# +# Builds a tiny WASIp1 command module that can be registered with: +# --tool echo=target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm + +set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] + +target := "wasm32-wasip1" +wasm := "target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm" + +build: + rustup target add {{target}} + cargo build --release --target {{target}} + +path: + @echo {{wasm}} + +clean: + cargo clean diff --git a/examples/echo-wasm-host-fxn/README.md b/examples/echo-wasm-host-fxn/README.md new file mode 100644 index 0000000..f8adada --- /dev/null +++ b/examples/echo-wasm-host-fxn/README.md @@ -0,0 +1,18 @@ +# echo-wasm-host-fxn + +A tiny WASIp1 command module used by the Python host-tools example as a custom host function. + +The Hyperlight host runs the module through `--tool echo=...`. The module receives the `__dispatch` request JSON on stdin: + +```json +{"name":"echo","args":{"message":"hello"}} +``` + +It writes `{"result": args}` to stdout, so the outer host dispatch returns the original `args` value as the tool result. + +```bash +just build +hyperlight-unikraft kernel --initrd app.cpio \ + --tool echo=target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm \ + -- /test_tools.py +``` diff --git a/examples/echo-wasm-host-fxn/src/main.rs b/examples/echo-wasm-host-fxn/src/main.rs new file mode 100644 index 0000000..b475443 --- /dev/null +++ b/examples/echo-wasm-host-fxn/src/main.rs @@ -0,0 +1,167 @@ +use std::io::{self, Read, Write}; + +fn main() { + let mut request = String::new(); + if let Err(err) = io::stdin().read_to_string(&mut request) { + write_error(&format!("failed to read request: {err}")); + return; + } + + match extract_args_json(&request) { + Ok(args) => print!("{{\"result\":{}}}", args.trim()), + Err(err) => write_error(err), + } +} + +fn extract_args_json(request: &str) -> Result<&str, &'static str> { + let bytes = request.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if bytes[i].is_ascii_whitespace() || bytes[i] == b'{' || bytes[i] == b',' { + i += 1; + continue; + } + + if bytes[i] != b'"' { + return Err("expected JSON object key"); + } + + let (key, next) = parse_string(request, i)?; + i = skip_ws(bytes, next); + if bytes.get(i) != Some(&b':') { + return Err("expected ':' after JSON object key"); + } + i = skip_ws(bytes, i + 1); + + let value_end = json_value_end(request, i)?; + if key == "args" { + return Ok(&request[i..value_end]); + } + i = value_end; + } + + Err("missing args field") +} + +fn parse_string(input: &str, start: usize) -> Result<(String, usize), &'static str> { + let bytes = input.as_bytes(); + if bytes.get(start) != Some(&b'"') { + return Err("expected JSON string"); + } + + let mut out = String::new(); + let mut i = start + 1; + while i < bytes.len() { + match bytes[i] { + b'"' => return Ok((out, i + 1)), + b'\\' => { + i += 1; + if i >= bytes.len() { + return Err("unterminated escape sequence"); + } + match bytes[i] { + b'"' => out.push('"'), + b'\\' => out.push('\\'), + b'/' => out.push('/'), + b'b' => out.push('\u{0008}'), + b'f' => out.push('\u{000c}'), + b'n' => out.push('\n'), + b'r' => out.push('\r'), + b't' => out.push('\t'), + b'u' => return Err("unicode escapes are not supported in object keys"), + _ => return Err("invalid escape sequence"), + } + } + byte => out.push(byte as char), + } + i += 1; + } + + Err("unterminated JSON string") +} + +fn json_value_end(input: &str, start: usize) -> Result { + let bytes = input.as_bytes(); + if start >= bytes.len() { + return Err("missing JSON value"); + } + + let mut i = start; + let mut depth = 0usize; + let mut in_string = false; + let mut escape = false; + + while i < bytes.len() { + let byte = bytes[i]; + if in_string { + if escape { + escape = false; + } else if byte == b'\\' { + escape = true; + } else if byte == b'"' { + in_string = false; + } + i += 1; + continue; + } + + match byte { + b'"' => in_string = true, + b'{' | b'[' => depth += 1, + b'}' | b']' => { + if depth == 0 { + return Ok(i); + } + depth -= 1; + if depth == 0 { + return Ok(i + 1); + } + } + b',' if depth == 0 => return Ok(i), + byte if byte.is_ascii_whitespace() && depth == 0 => return Ok(i), + _ => {} + } + i += 1; + } + + if in_string || depth != 0 { + Err("unterminated JSON value") + } else { + Ok(i) + } +} + +fn skip_ws(bytes: &[u8], mut i: usize) -> usize { + while i < bytes.len() && bytes[i].is_ascii_whitespace() { + i += 1; + } + i +} + +fn write_error(message: &str) { + let escaped = message.replace('\\', "\\\\").replace('"', "\\\""); + let _ = write!(io::stdout(), "{{\"error\":\"{escaped}\"}}"); +} + +#[cfg(test)] +mod tests { + use super::extract_args_json; + + #[test] + fn extracts_args_object() { + let request = r#"{"name":"echo","args":{"message":"hello"}}"#; + assert_eq!( + extract_args_json(request).unwrap(), + r#"{"message":"hello"}"# + ); + } + + #[test] + fn extracts_args_when_first() { + let request = r#"{"args":["hello",{"nested":true}],"name":"echo"}"#; + assert_eq!( + extract_args_json(request).unwrap(), + r#"["hello",{"nested":true}]"# + ); + } +} diff --git a/examples/python-tools/Justfile b/examples/python-tools/Justfile index a3ad472..ad8730d 100644 --- a/examples/python-tools/Justfile +++ b/examples/python-tools/Justfile @@ -15,13 +15,19 @@ memory := "512Mi" image := "python-tools-hyperlight" ghcr_kernel := "ghcr.io/hyperlight-dev/hyperlight-unikraft/python-tools-kernel:latest" +echo_tool := "../echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm" + +# Build the Wasm host function used by --tool echo=... +echo-tool: + just -d ../echo-wasm-host-fxn build + # Run the example -run: - hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --enable-tools -- /test_tools.py +run: echo-tool + hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --tool echo={{echo_tool}} -- /test_tools.py # Run 10 times via snapshot/restore -run-10: - hyperlight-unikraft {{kernel}} --initrd {{initrd}} --repeat 9 --memory {{memory}} --enable-tools -- /test_tools.py +run-10: echo-tool + hyperlight-unikraft {{kernel}} --initrd {{initrd}} --repeat 9 --memory {{memory}} --tool echo={{echo_tool}} -- /test_tools.py # Build rootfs via Docker (cross-platform) rootfs: diff --git a/host/Cargo.lock b/host/Cargo.lock index acfa292..bf20426 100644 --- a/host/Cargo.lock +++ b/host/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -17,6 +26,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -82,6 +103,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arrayref" version = "0.3.9" @@ -90,9 +117,20 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "autocfg" @@ -143,12 +181,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] @@ -166,6 +204,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytemuck" @@ -189,15 +230,93 @@ dependencies = [ [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.4", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand 0.8.6", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] [[package]] name = "cc" -version = "1.2.64" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -219,13 +338,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -279,6 +398,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -315,6 +443,144 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd990d8a6304475bbad64534a0d418f5572f44d5f011437e6b9f1ee7d5c2570" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccabe4636007296721080e02d7dab46d4319638ec4e3f6f7402fcb46dc5122c6" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7ed173c870c0aea202a9830880156905a028a88df076e35ce383a8acbf90a7" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "800cc586df98b12c502e76707c96565e40629a5322eaa15aaa34ba05f5721e31" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae93f863f9094ae34d2567f9edb0ae2c41d35228b286598354dd78b198868ebd" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c505162bcf77dcb859905b3eac56a1917fc3cf326424fb06e7732031e3a8ae" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b786958bcb79bdb5fbae095af58f0c2da7d7895c475c991f6a6bb5a9c7e6d9" + +[[package]] +name = "cranelift-control" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2faf9a5009bce7f725ce2af7a08c4883ebac6af933e7e0aa7d84f976f4e6deb5" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "017271194ba5e101d626560d0d6767efd341468d1ba0f4d015f19fe64020b65b" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f80847f0929967f0cec82f9e0543b3901e0f0063690405891f22107b5a130fd8" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75904abbc0e7b46d20f7a49c8042c8a4481c0db4253b99889c723c566295d506" + +[[package]] +name = "cranelift-native" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0135923540574362e16f01bf40000664263840991039ff3041ba717de6cf3a" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.123.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fb12f76c482e034f6ebefa843c914e74112f088215d8d36d33a649f9fab99b" + [[package]] name = "crc32fast" version = "1.5.0" @@ -380,6 +646,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -396,6 +700,29 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "filetime" version = "0.2.29" @@ -438,12 +765,68 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.32" @@ -457,6 +840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-sink", "futures-task", "pin-project-lite", "slab", @@ -497,16 +881,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core", - "wasip2", - "wasip3", + "rand_core 0.10.1", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", ] [[package]] @@ -552,6 +945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", + "serde", ] [[package]] @@ -591,7 +985,7 @@ dependencies = [ "flatbuffers", "log", "spin", - "thiserror", + "thiserror 2.0.18", "tracing", "tracing-core", ] @@ -621,11 +1015,11 @@ dependencies = [ "mshv-bindings", "mshv-ioctls", "page_size", - "rand", + "rand 0.10.1", "rust-embed", "serde_json", "termcolor", - "thiserror", + "thiserror 2.0.18", "tracing", "tracing-core", "tracing-log", @@ -650,9 +1044,12 @@ dependencies = [ "memmap2", "nix", "serde_json", - "socket2", + "socket2 0.5.10", "tar", + "tempfile", "ureq", + "wasmtime", + "wasmtime-wasi", "windows-sys 0.61.2", ] @@ -681,67 +1078,201 @@ dependencies = [ ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "icu_collections" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] [[package]] -name = "indexmap" -version = "2.14.0" +name = "icu_locale_core" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "icu_normalizer" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] [[package]] -name = "itoa" -version = "1.0.18" +name = "icu_normalizer_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] -name = "jobserver" -version = "0.1.34" +name = "icu_properties" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "getrandom 0.3.4", - "libc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", ] [[package]] -name = "js-sys" -version = "0.3.102" +name = "icu_properties_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] -name = "kvm-bindings" -version = "0.14.1" +name = "icu_provider" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11cf0ca75d59e9d298647c59cf6c5286fa048120caa77972a7a504a0824d234f" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ - "vmm-sys-util", + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] -name = "kvm-ioctls" +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kvm-bindings" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11cf0ca75d59e9d298647c59cf6c5286fa048120caa77972a7a504a0824d234f" +dependencies = [ + "vmm-sys-util", +] + +[[package]] +name = "kvm-ioctls" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "333f77a20344a448f3f70664918135fddeb804e938f28a99d685bd92926e0b19" @@ -758,6 +1289,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -782,6 +1319,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.17" @@ -803,12 +1346,24 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "lock_api" version = "0.4.14" @@ -820,9 +1375,24 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" [[package]] name = "memchr" @@ -830,11 +1400,20 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -859,6 +1438,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "mshv-bindings" version = "0.6.9" @@ -879,7 +1469,7 @@ checksum = "1db4449ac7012237b133da366f5b32ce4af1f8caf770486e5a9d54f7f6b73c4c" dependencies = [ "libc", "mshv-bindings", - "thiserror", + "thiserror 2.0.18", "vmm-sys-util", ] @@ -925,6 +1515,18 @@ dependencies = [ "syn", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -984,13 +1586,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] -name = "prettyplease" -version = "0.2.37" +name = "postcard" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" dependencies = [ - "proc-macro2", - "syn", + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", ] [[package]] @@ -1002,11 +1624,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulley-interpreter" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558181096e0df4984f45cfc3a7087052df4a61c36089b135a08ceca9cbd352fb" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d52e2f14e168d75cdabe9bd5fb1ff18a1b119dc6699684aee895dbc3524da9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1023,6 +1668,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.1" @@ -1030,8 +1686,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.2", - "rand_core", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1057,7 +1732,21 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", ] [[package]] @@ -1127,6 +1816,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1136,6 +1831,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1145,15 +1853,25 @@ dependencies = [ "bitflags 2.13.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "log", "once_cell", @@ -1230,6 +1948,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -1238,6 +1960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -1311,6 +2034,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.10" @@ -1321,6 +2053,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "spin" version = "0.10.0" @@ -1330,6 +2072,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -1353,6 +2101,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags 2.13.0", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + [[package]] name = "tar" version = "0.4.46" @@ -1364,6 +2139,25 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1375,15 +2169,35 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", ] [[package]] -name = "thiserror-impl" +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" @@ -1393,6 +2207,30 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.4", + "windows-sys 0.61.2", +] + [[package]] name = "tracing" version = "0.1.44" @@ -1490,12 +2328,30 @@ dependencies = [ "log", ] +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf8-zero" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1504,11 +2360,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -1563,23 +2419,14 @@ version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1590,9 +2437,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1600,9 +2447,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -1613,45 +2460,334 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" -version = "0.244.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7" dependencies = [ "leb128fmt", "wasmparser", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "wasmparser" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" dependencies = [ - "anyhow", + "bitflags 2.13.0", + "hashbrown 0.15.5", "indexmap", - "wasm-encoder", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1" +dependencies = [ + "anyhow", + "termcolor", "wasmparser", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "wasmtime" +version = "36.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "4b4442dc12aa2473def8334f0e0f2b489be52c52507c938bbdc8be69ded4ded6" dependencies = [ + "addr2line", + "anyhow", + "async-trait", "bitflags 2.13.0", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", "hashbrown 0.15.5", "indexmap", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-asm-macros", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-environ" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d881c3d6205898a226cc487b117f23b9ed1c7da39952d65bd5eeb6745b3789c" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object", + "postcard", "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-internal-component-util", +] + +[[package]] +name = "wasmtime-internal-asm-macros" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab1876bcfa51d6a05dea1c13933f53cbc1e316c783fddebc859f56a736eae07" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae1407944a0b13a8a77930b5b951aa7134beccecad7efac1ef9f03adb7d1a0f" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "646a53678ce6aaf6f097e18ca51f650f2841aea6d2bcd7b61931397b8b8f30db" + +[[package]] +name = "wasmtime-internal-cranelift" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3495aa8300e4ca6b53f81a53ce5eff6621fd5ff8378ef9ae552d1479d57371" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5e4023a6b167da157338f5f0f505945eb45e78f1cac2d4dcce0922457d7d4" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-internal-asm-macros", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da71e2d573e3cc6f753a3b7bff98f425ca060c0e8071cc55c3d867a9edf3ecc" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "627d8f57909a4f9bb1dbe57a96229a54b89d5995353d0b321f3cb9a1a118977a" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b99315585a8a27125dd9b0150edb115d6f6ff0baae453c21d30822aab77f00" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-internal-slab" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaee97281dd3fe47ec3d46c16fb9fe2dd32f37d0523c2d5c484f11b348734e4" + +[[package]] +name = "wasmtime-internal-unwinder" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c005f82c48492b6b44fa19ee5205bd933c4f8baca41e314eca8331dd3c4fd9" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "log", + "object", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b73639a9c0c0e33a2ef942ca99b6772b48393be92bebbd0767c607e5b0a68e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "392ca021d084c7426616ef77e1284315555f11bcbb34f416d74b0732db622811" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd4703351476262d715b72431e80d10289908e3494050071d6521267f522d97" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "heck", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wasmtime-wasi" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21921b6e8e8ed876a288cb3b0b3aee68809fed8182ce26c9977ffc4af4cb77f6" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.13.0", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", + "system-interface", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cceb2d110d8de61e7b0e8c501b838d3c6403a14cdca8cb612ff1228db537c6d" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", ] [[package]] @@ -1663,6 +2799,47 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wiggle" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a0751406b641ff50ef42d4a1ca843a03040c488c0c27f92093633447464013" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.13.0", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab62083fdcecdd0cac61b8c46e7de4f2629ebe8699fd9ce790d922cc89d50f5f" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "syn", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756b7a4a7f57ee2f53e9ef3501ed0faacda4b8dcb169a921cddc8bc09ebd199e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1694,6 +2871,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "36.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ec880b20caaa72245944b54cfb22aca111f8c805e12a7542b40d66921e5323" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", +] + [[package]] name = "windows" version = "0.62.2" @@ -1801,7 +2998,25 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -1819,14 +3034,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1853,42 +3085,84 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1896,12 +3170,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winx" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "wit-bindgen-rust-macro", + "bitflags 2.13.0", + "windows-sys 0.59.0", ] [[package]] @@ -1911,92 +3192,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "wit-bindgen-core" -version = "0.51.0" +name = "wit-parser" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" dependencies = [ "anyhow", - "heck", - "wit-parser", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] -name = "wit-bindgen-rust" -version = "0.51.0" +name = "witx" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" dependencies = [ "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "log", + "thiserror 1.0.69", + "wast", ] [[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "writeable" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "wit-component" -version = "0.244.0" +name = "xattr" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "libc", + "rustix 1.1.4", ] [[package]] -name = "wit-parser" -version = "0.244.0" +name = "yoke" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "xattr" -version = "1.6.1" +name = "yoke-derive" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ - "libc", - "rustix", + "proc-macro2", + "quote", + "syn", + "synstructure", ] [[package]] @@ -2019,12 +3280,66 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/host/Cargo.toml b/host/Cargo.toml index c312f67..2c75c30 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -26,6 +26,10 @@ path = "src/bin/pydriver_run.rs" name = "pyhl" path = "src/bin/pyhl.rs" +[features] +default = [] +wasm-host-fns = ["dep:wasmtime", "dep:wasmtime-wasi"] + [dependencies] # danbugs/hyperlight perf/whp-warm-start — snapshot file support + WHP warm-start optimizations. hyperlight-host = { git = "https://github.com/danbugs/hyperlight", rev = "5cf37d92", features = ["executable_heap", "hw-interrupts", "whp-no-surrogate"] } @@ -38,6 +42,8 @@ socket2 = { version = "0.5", features = ["all"] } ureq = "3" flate2 = "1" tar = "0.4" +wasmtime = { version = "36.0.2", optional = true, default-features = false, features = ["cranelift", "runtime", "std"] } +wasmtime-wasi = { version = "36.0.2", optional = true, default-features = false, features = ["preview1"] } [target.'cfg(unix)'.dependencies] nix = { version = "0.29", features = ["fs"] } @@ -46,3 +52,5 @@ libc = "0.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_System_IO", "Win32_System_Ioctl", "Win32_Storage_FileSystem", "Win32_Networking_WinSock"] } +[dev-dependencies] +tempfile = "3" diff --git a/host/src/main.rs b/host/src/main.rs index 98f2c4e..593b17c 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -13,6 +13,9 @@ use hyperlight_unikraft::{ }; use std::path::PathBuf; +#[cfg(feature = "wasm-host-fns")] +mod wasm_host_fns; + #[derive(Parser, Debug)] #[command( name = "hyperlight-unikraft", @@ -39,9 +42,63 @@ struct Args { #[arg(long, short = 'q')] quiet: bool, - /// Enable tool dispatch via __dispatch host function - #[arg(long)] - enable_tools: bool, + #[cfg(feature = "wasm-host-fns")] + #[arg( + long = "tool", + value_name = "NAME=WASM", + help = "Register a WASIp1 module as a host tool" + )] + tool: Vec, + + #[cfg(feature = "wasm-host-fns")] + #[arg( + long = "tool-wasi-dir", + value_name = "HOST[:GUEST]", + help = "Preopen a read-write host directory for Wasm tools" + )] + tool_wasi_dir: Vec, + + #[cfg(feature = "wasm-host-fns")] + #[arg( + long = "tool-wasi-dir-ro", + value_name = "HOST[:GUEST]", + help = "Preopen a read-only host directory for Wasm tools" + )] + tool_wasi_dir_ro: Vec, + + #[cfg(feature = "wasm-host-fns")] + #[arg( + long = "tool-wasi-env", + value_name = "KEY=VALUE", + help = "Set an environment variable for Wasm tools" + )] + tool_wasi_env: Vec, + + #[cfg(feature = "wasm-host-fns")] + #[arg( + long = "tool-wasi-env-inherit", + value_name = "KEY", + help = "Inherit one host environment variable into Wasm tools" + )] + tool_wasi_env_inherit: Vec, + + #[cfg(feature = "wasm-host-fns")] + #[arg( + long = "tool-wasi-fuel", + default_value_t = 100_000_000, + value_name = "FUEL", + help = "Fuel units available to each Wasm tool call" + )] + tool_wasi_fuel: u64, + + #[cfg(feature = "wasm-host-fns")] + #[arg( + long = "tool-wasi-output-limit", + default_value = "1Mi", + value_name = "SIZE", + help = "Maximum stdout or stderr captured from one Wasm tool call" + )] + tool_wasi_output_limit: String, /// Preopen a host directory for the guest's sandboxed filesystem. /// @@ -206,6 +263,48 @@ fn main() -> Result<()> { None }; + #[cfg(feature = "wasm-host-fns")] + let wasm_tools = { + if args.tool.is_empty() + && wasm_host_fns::WasmToolOptions::has_capabilities( + &args.tool_wasi_dir, + &args.tool_wasi_dir_ro, + &args.tool_wasi_env, + &args.tool_wasi_env_inherit, + ) + { + return Err(anyhow::anyhow!( + "--tool-wasi-* flags require at least one --tool" + )); + } + if args.tool.is_empty() { + Vec::new() + } else { + let output_limit = parse_memory(&args.tool_wasi_output_limit)?; + let output_limit = usize::try_from(output_limit).map_err(|_| { + anyhow::anyhow!( + "--tool-wasi-output-limit too large: {}", + args.tool_wasi_output_limit + ) + })?; + let options = wasm_host_fns::WasmToolOptions::from_cli( + &args.tool_wasi_dir, + &args.tool_wasi_dir_ro, + &args.tool_wasi_env, + &args.tool_wasi_env_inherit, + args.tool_wasi_fuel, + output_limit, + )?; + let tools = wasm_host_fns::WasmTool::load_all(&args.tool, &options)?; + if !args.quiet { + for tool in &tools { + eprintln!("Tool: {} -> {}", tool.name(), tool.path().display()); + } + } + tools + } + }; + let mut builder = Sandbox::builder(&args.kernel) .args(app_args) .heap_size(heap_size) @@ -222,8 +321,11 @@ fn main() -> Result<()> { if let Some(ports) = listen_ports { builder = builder.listen_ports(ports); } - if args.enable_tools { - builder = builder.tool("echo", Ok); + #[cfg(feature = "wasm-host-fns")] + for tool in wasm_tools { + let name = tool.name().to_string(); + let tool = std::sync::Arc::new(tool); + builder = builder.tool(&name, move |args| tool.invoke(args)); } let mut sandbox = builder.build()?; let evolve_time = t0.elapsed(); diff --git a/host/src/wasm_host_fns.rs b/host/src/wasm_host_fns.rs new file mode 100644 index 0000000..cbf920f --- /dev/null +++ b/host/src/wasm_host_fns.rs @@ -0,0 +1,1124 @@ +use anyhow::{anyhow, bail, Context, Result}; +use serde_json::Value; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use wasmtime::{Config, Engine, InstancePre, Linker, Module, Store}; +use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe}; +use wasmtime_wasi::preview1::{self, WasiP1Ctx}; +use wasmtime_wasi::{DirPerms, FilePerms, I32Exit, WasiCtxBuilder}; + +#[derive(Clone)] +pub struct WasmToolOptions { + dirs: Vec, + env: Vec<(String, String)>, + fuel: u64, + output_limit: usize, +} + +#[derive(Clone)] +struct WasiDir { + host: PathBuf, + guest: String, + read_only: bool, +} + +pub struct WasmTool { + name: String, + path: PathBuf, + engine: Engine, + pre: InstancePre, + options: WasmToolOptions, +} + +impl WasmToolOptions { + pub fn from_cli( + rw_dirs: &[String], + ro_dirs: &[String], + env: &[String], + inherit_env: &[String], + fuel: u64, + output_limit: usize, + ) -> Result { + if fuel == 0 { + bail!("--tool-wasi-fuel must be greater than 0"); + } + if output_limit == 0 { + bail!("--tool-wasi-output-limit must be greater than 0"); + } + + let mut dirs = Vec::with_capacity(rw_dirs.len() + ro_dirs.len()); + for spec in rw_dirs { + dirs.push(parse_wasi_dir(spec, false)?); + } + for spec in ro_dirs { + dirs.push(parse_wasi_dir(spec, true)?); + } + let mut guest_paths = HashSet::new(); + for dir in &dirs { + if !guest_paths.insert(dir.guest.clone()) { + bail!("duplicate Wasm tool WASI guest path: {}", dir.guest); + } + } + + let mut merged_env = Vec::with_capacity(env.len() + inherit_env.len()); + for key in inherit_env { + if key.is_empty() { + bail!("--tool-wasi-env-inherit key must not be empty"); + } + let value = std::env::var(key) + .with_context(|| format!("inherit environment variable {key}"))?; + set_env_pair(&mut merged_env, key.clone(), value); + } + for spec in env { + let (key, value) = parse_env_pair(spec)?; + set_env_pair(&mut merged_env, key, value); + } + + Ok(Self { + dirs, + env: merged_env, + fuel, + output_limit, + }) + } + + pub fn has_capabilities( + rw_dirs: &[String], + ro_dirs: &[String], + env: &[String], + inherit_env: &[String], + ) -> bool { + !rw_dirs.is_empty() || !ro_dirs.is_empty() || !env.is_empty() || !inherit_env.is_empty() + } +} + +impl WasmTool { + pub fn load_all(specs: &[String], options: &WasmToolOptions) -> Result> { + let mut config = Config::new(); + config.consume_fuel(true); + let engine = Engine::new(&config)?; + let mut seen = HashSet::new(); + let mut tools = Vec::with_capacity(specs.len()); + + for spec in specs { + let (name, path) = parse_tool_spec(spec)?; + if !seen.insert(name.clone()) { + bail!("duplicate --tool name: {name}"); + } + let path = std::fs::canonicalize(&path) + .with_context(|| format!("canonicalize Wasm tool {}", path.display()))?; + let module = Module::from_file(&engine, &path) + .with_context(|| format!("compile Wasm tool {}", path.display()))?; + let mut linker: Linker = Linker::new(&engine); + preview1::add_to_linker_sync(&mut linker, |ctx| ctx)?; + let pre = linker + .instantiate_pre(&module) + .with_context(|| format!("link Wasm tool {}", path.display()))?; + tools.push(Self { + name, + path, + engine: engine.clone(), + pre, + options: options.clone(), + }); + } + + Ok(tools) + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn invoke(&self, args: Value) -> Result { + let request = serde_json::json!({ "name": &self.name, "args": args }); + let stdin = serde_json::to_vec(&request)?; + let stdout = MemoryOutputPipe::new(self.options.output_limit); + let stderr = MemoryOutputPipe::new(self.options.output_limit); + + let mut builder = WasiCtxBuilder::new(); + builder + .allow_blocking_current_thread(true) + .arg(self.path.to_string_lossy()) + .arg(&self.name) + .stdin(MemoryInputPipe::new(stdin)) + .stdout(stdout.clone()) + .stderr(stderr.clone()); + + for (key, value) in &self.options.env { + builder.env(key, value); + } + for dir in &self.options.dirs { + let dir_perms = if dir.read_only { + DirPerms::READ + } else { + DirPerms::all() + }; + let file_perms = if dir.read_only { + FilePerms::READ + } else { + FilePerms::all() + }; + builder + .preopened_dir(&dir.host, &dir.guest, dir_perms, file_perms) + .with_context(|| format!("preopen {} as {}", dir.host.display(), dir.guest))?; + } + + let wasi = builder.build_p1(); + let mut store = Store::new(&self.engine, wasi); + store.set_fuel(self.options.fuel)?; + + let instance = self.pre.instantiate(&mut store)?; + let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?; + let status = match start.call(&mut store, ()) { + Ok(()) => 0, + Err(err) => { + if let Some(exit) = err.downcast_ref::() { + exit.0 + } else { + let stderr_text = pipe_text(&stderr); + if stderr_text.trim().is_empty() { + return Err(err) + .with_context(|| format!("Wasm tool {} trapped", self.name)); + } + return Err(err).with_context(|| { + format!( + "Wasm tool {} trapped; stderr: {}", + self.name, + stderr_text.trim() + ) + }); + } + } + }; + + let stdout_bytes = stdout.contents(); + let stdout_len = stdout_bytes.len(); + let stdout_text = String::from_utf8_lossy(&stdout_bytes).into_owned(); + let stderr_text = pipe_text(&stderr); + if status != 0 { + if stderr_text.trim().is_empty() { + bail!("Wasm tool {} exited with status {status}", self.name); + } + bail!( + "Wasm tool {} exited with status {status}; stderr: {}", + self.name, + stderr_text.trim() + ); + } + + match parse_tool_stdout(&self.name, &stdout_text) { + Ok(value) => Ok(value), + Err(err) if stdout_len >= self.options.output_limit => Err(err).with_context(|| { + format!( + "Wasm tool {} stdout may have reached output limit of {} bytes", + self.name, self.options.output_limit + ) + }), + Err(err) => Err(err), + } + } +} + +fn parse_tool_spec(spec: &str) -> Result<(String, PathBuf)> { + let (name, path) = spec + .split_once('=') + .ok_or_else(|| anyhow!("--tool must use NAME=WASM syntax: {spec}"))?; + let name = name.trim(); + let path = path.trim(); + if name.is_empty() { + bail!("--tool name must not be empty: {spec}"); + } + if name.starts_with("__") || name.starts_with("fs_") || name.starts_with("net_") { + bail!("--tool name {name} is reserved"); + } + if path.is_empty() { + bail!("--tool path must not be empty: {spec}"); + } + Ok((name.to_string(), PathBuf::from(path))) +} + +fn parse_wasi_dir(spec: &str, read_only: bool) -> Result { + let (host, guest) = if let Some(idx) = spec.rfind(':') { + let (host, guest) = spec.split_at(idx); + let guest = &guest[1..]; + if is_windows_drive_path(spec, idx) { + (spec, "/host") + } else if guest.starts_with('/') || guest == "." || guest.starts_with("./") { + (host, guest) + } else { + bail!( + "invalid WASI preopen guest path {:?}: expected absolute path, '.', or './path'", + guest + ); + } + } else { + (spec, "/host") + }; + + if host.is_empty() { + bail!("WASI preopen host path must not be empty: {spec}"); + } + if guest.is_empty() { + bail!("WASI preopen guest path must not be empty: {spec}"); + } + + let host = std::fs::canonicalize(host) + .with_context(|| format!("canonicalize WASI preopen host path {host}"))?; + Ok(WasiDir { + host, + guest: guest.to_string(), + read_only, + }) +} + +fn parse_env_pair(spec: &str) -> Result<(String, String)> { + let (key, value) = spec + .split_once('=') + .ok_or_else(|| anyhow!("--tool-wasi-env must use KEY=VALUE syntax: {spec}"))?; + if key.is_empty() { + bail!("--tool-wasi-env key must not be empty: {spec}"); + } + Ok((key.to_string(), value.to_string())) +} + +fn set_env_pair(env: &mut Vec<(String, String)>, key: String, value: String) { + if let Some((_, existing)) = env + .iter_mut() + .find(|(existing_key, _)| existing_key == &key) + { + *existing = value; + } else { + env.push((key, value)); + } +} + +fn is_windows_drive_path(spec: &str, colon_idx: usize) -> bool { + colon_idx == 1 + && spec + .as_bytes() + .first() + .map(|b| b.is_ascii_alphabetic()) + .unwrap_or(false) + && spec + .as_bytes() + .get(2) + .map(|b| *b == b'/' || *b == b'\\') + .unwrap_or(false) +} + +fn pipe_text(pipe: &MemoryOutputPipe) -> String { + String::from_utf8_lossy(&pipe.contents()).into_owned() +} + +fn parse_tool_stdout(name: &str, stdout: &str) -> Result { + let trimmed = stdout.trim(); + if trimmed.is_empty() { + return Ok(Value::Null); + } + + let value: Value = serde_json::from_str(trimmed) + .with_context(|| format!("Wasm tool {name} wrote non-JSON stdout"))?; + if let Some(object) = value.as_object() { + if object.len() == 1 { + if let Some(result) = object.get("result") { + return Ok(result.clone()); + } + if let Some(error) = object.get("error") { + if let Some(message) = error.as_str() { + bail!("Wasm tool {name}: {message}"); + } + bail!("Wasm tool {name}: {error}"); + } + } + } + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::fs; + use tempfile::TempDir; + + fn tempdir(label: &str) -> TempDir { + tempfile::Builder::new() + .prefix(&format!("hl-wasm-tools-{label}-")) + .tempdir() + .unwrap() + } + + fn default_options() -> WasmToolOptions { + WasmToolOptions::from_cli(&[], &[], &[], &[], 1_000_000, 4096).unwrap() + } + + fn options_with_limits(fuel: u64, output_limit: usize) -> WasmToolOptions { + WasmToolOptions::from_cli(&[], &[], &[], &[], fuel, output_limit).unwrap() + } + + fn load_tool(name: &str, wasm: Vec, options: WasmToolOptions) -> WasmTool { + let dir = tempdir(name); + let path = dir.path().join(format!("{name}.wasm")); + fs::write(&path, wasm).unwrap(); + let specs = vec![format!("{name}={}", path.display())]; + let mut tools = WasmTool::load_all(&specs, &options).unwrap(); + assert_eq!(tools.len(), 1); + tools.remove(0) + } + + fn err_string(err: anyhow::Error) -> String { + format!("{err:#}") + } + + fn encode_u32(mut value: u32, out: &mut Vec) { + loop { + let mut byte = (value & 0x7f) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + out.push(byte); + if value == 0 { + break; + } + } + } + + fn encode_i32(mut value: i32, out: &mut Vec) { + loop { + let byte = (value as u8) & 0x7f; + value >>= 7; + let done = (value == 0 && (byte & 0x40) == 0) || (value == -1 && (byte & 0x40) != 0); + if done { + out.push(byte); + break; + } + out.push(byte | 0x80); + } + } + + fn encode_i64(mut value: i64, out: &mut Vec) { + loop { + let byte = (value as u8) & 0x7f; + value >>= 7; + let done = (value == 0 && (byte & 0x40) == 0) || (value == -1 && (byte & 0x40) != 0); + if done { + out.push(byte); + break; + } + out.push(byte | 0x80); + } + } + + fn push_name(out: &mut Vec, name: &str) { + encode_u32(name.len() as u32, out); + out.extend_from_slice(name.as_bytes()); + } + + fn push_section(module: &mut Vec, id: u8, payload: Vec) { + module.push(id); + encode_u32(payload.len() as u32, module); + module.extend(payload); + } + + fn func_type(params: &[u8], results: &[u8]) -> Vec { + let mut out = vec![0x60]; + encode_u32(params.len() as u32, &mut out); + out.extend_from_slice(params); + encode_u32(results.len() as u32, &mut out); + out.extend_from_slice(results); + out + } + + fn i32_const(out: &mut Vec, value: i32) { + out.push(0x41); + encode_i32(value, out); + } + + fn i64_const(out: &mut Vec, value: i64) { + out.push(0x42); + encode_i64(value, out); + } + + fn i32_store(out: &mut Vec) { + out.push(0x36); + encode_u32(2, out); + encode_u32(0, out); + } + + fn i32_load(out: &mut Vec) { + out.push(0x28); + encode_u32(2, out); + encode_u32(0, out); + } + + fn call(out: &mut Vec, index: u32) { + out.push(0x10); + encode_u32(index, out); + } + + fn drop_value(out: &mut Vec) { + out.push(0x1a); + } + + fn end(out: &mut Vec) { + out.push(0x0b); + } + + fn function_body(mut instructions: Vec) -> Vec { + let mut body = vec![0x00]; + if !instructions.ends_with(&[0x0b]) { + instructions.push(0x0b); + } + body.extend(instructions); + body + } + + fn module( + types: Vec>, + imports: Vec<(&str, &str, u32)>, + functions: Vec, + export_start_index: u32, + memory: bool, + bodies: Vec>, + data: Vec<(u32, Vec)>, + ) -> Vec { + let mut module = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; + + let mut type_payload = Vec::new(); + encode_u32(types.len() as u32, &mut type_payload); + for ty in types { + type_payload.extend(ty); + } + push_section(&mut module, 1, type_payload); + + if !imports.is_empty() { + let mut import_payload = Vec::new(); + encode_u32(imports.len() as u32, &mut import_payload); + for (module_name, field_name, type_index) in imports { + push_name(&mut import_payload, module_name); + push_name(&mut import_payload, field_name); + import_payload.push(0x00); + encode_u32(type_index, &mut import_payload); + } + push_section(&mut module, 2, import_payload); + } + + let mut function_payload = Vec::new(); + encode_u32(functions.len() as u32, &mut function_payload); + for type_index in functions { + encode_u32(type_index, &mut function_payload); + } + push_section(&mut module, 3, function_payload); + + if memory { + let memory_payload = vec![0x01, 0x00, 0x01]; + push_section(&mut module, 5, memory_payload); + } + + let mut export_payload = Vec::new(); + encode_u32(if memory { 2 } else { 1 }, &mut export_payload); + push_name(&mut export_payload, "_start"); + export_payload.push(0x00); + encode_u32(export_start_index, &mut export_payload); + if memory { + push_name(&mut export_payload, "memory"); + export_payload.push(0x02); + encode_u32(0, &mut export_payload); + } + push_section(&mut module, 7, export_payload); + + let mut code_payload = Vec::new(); + encode_u32(bodies.len() as u32, &mut code_payload); + for body in bodies { + encode_u32(body.len() as u32, &mut code_payload); + code_payload.extend(body); + } + push_section(&mut module, 10, code_payload); + + if !data.is_empty() { + let mut data_payload = Vec::new(); + encode_u32(data.len() as u32, &mut data_payload); + for (offset, bytes) in data { + data_payload.push(0x00); + i32_const(&mut data_payload, offset as i32); + end(&mut data_payload); + encode_u32(bytes.len() as u32, &mut data_payload); + data_payload.extend(bytes); + } + push_section(&mut module, 11, data_payload); + } + + module + } + + fn stdout_module(bytes: &[u8]) -> Vec { + let mut instructions = Vec::new(); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 64); + i32_store(&mut instructions); + i32_const(&mut instructions, 12); + i32_const(&mut instructions, bytes.len() as i32); + i32_store(&mut instructions); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 4); + call(&mut instructions, 0); + drop_value(&mut instructions); + end(&mut instructions); + + module( + vec![ + func_type(&[0x7f, 0x7f, 0x7f, 0x7f], &[0x7f]), + func_type(&[], &[]), + ], + vec![("wasi_snapshot_preview1", "fd_write", 0)], + vec![1], + 1, + true, + vec![function_body(instructions)], + vec![(64, bytes.to_vec())], + ) + } + + fn stdin_echo_module() -> Vec { + let mut instructions = Vec::new(); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 64); + i32_store(&mut instructions); + i32_const(&mut instructions, 12); + i32_const(&mut instructions, 2048); + i32_store(&mut instructions); + i32_const(&mut instructions, 0); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 4); + call(&mut instructions, 0); + drop_value(&mut instructions); + i32_const(&mut instructions, 16); + i32_const(&mut instructions, 64); + i32_store(&mut instructions); + i32_const(&mut instructions, 20); + i32_const(&mut instructions, 4); + i32_load(&mut instructions); + i32_store(&mut instructions); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 16); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 24); + call(&mut instructions, 1); + drop_value(&mut instructions); + end(&mut instructions); + + module( + vec![ + func_type(&[0x7f, 0x7f, 0x7f, 0x7f], &[0x7f]), + func_type(&[], &[]), + ], + vec![ + ("wasi_snapshot_preview1", "fd_read", 0), + ("wasi_snapshot_preview1", "fd_write", 0), + ], + vec![1], + 2, + true, + vec![function_body(instructions)], + Vec::new(), + ) + } + + fn env_value_module(key: &str, value_len: usize) -> Vec { + let mut instructions = Vec::new(); + i32_const(&mut instructions, 32); + i32_const(&mut instructions, 128); + call(&mut instructions, 0); + drop_value(&mut instructions); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 128 + key.len() as i32 + 1); + i32_store(&mut instructions); + i32_const(&mut instructions, 12); + i32_const(&mut instructions, value_len as i32); + i32_store(&mut instructions); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 4); + call(&mut instructions, 1); + drop_value(&mut instructions); + end(&mut instructions); + + module( + vec![ + func_type(&[0x7f, 0x7f], &[0x7f]), + func_type(&[0x7f, 0x7f, 0x7f, 0x7f], &[0x7f]), + func_type(&[], &[]), + ], + vec![ + ("wasi_snapshot_preview1", "environ_get", 0), + ("wasi_snapshot_preview1", "fd_write", 1), + ], + vec![2], + 2, + true, + vec![function_body(instructions)], + Vec::new(), + ) + } + + fn preopen_read_file_module(path: &[u8]) -> Vec { + let mut instructions = Vec::new(); + i32_const(&mut instructions, 3); + i32_const(&mut instructions, 0); + i32_const(&mut instructions, 64); + i32_const(&mut instructions, path.len() as i32); + i32_const(&mut instructions, 0); + i64_const(&mut instructions, 2); + i64_const(&mut instructions, 0); + i32_const(&mut instructions, 0); + i32_const(&mut instructions, 4); + call(&mut instructions, 0); + drop_value(&mut instructions); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 128); + i32_store(&mut instructions); + i32_const(&mut instructions, 12); + i32_const(&mut instructions, 512); + i32_store(&mut instructions); + i32_const(&mut instructions, 4); + i32_load(&mut instructions); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 20); + call(&mut instructions, 1); + drop_value(&mut instructions); + i32_const(&mut instructions, 24); + i32_const(&mut instructions, 128); + i32_store(&mut instructions); + i32_const(&mut instructions, 28); + i32_const(&mut instructions, 20); + i32_load(&mut instructions); + i32_store(&mut instructions); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 24); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 32); + call(&mut instructions, 2); + drop_value(&mut instructions); + end(&mut instructions); + + module( + vec![ + func_type( + &[0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7f, 0x7f], + &[0x7f], + ), + func_type(&[0x7f, 0x7f, 0x7f, 0x7f], &[0x7f]), + func_type(&[], &[]), + ], + vec![ + ("wasi_snapshot_preview1", "path_open", 0), + ("wasi_snapshot_preview1", "fd_read", 1), + ("wasi_snapshot_preview1", "fd_write", 1), + ], + vec![2], + 3, + true, + vec![function_body(instructions)], + vec![(64, path.to_vec())], + ) + } + + fn no_output_module() -> Vec { + module( + vec![func_type(&[], &[])], + Vec::new(), + vec![0], + 0, + false, + vec![function_body(vec![0x0b])], + Vec::new(), + ) + } + + fn infinite_loop_module() -> Vec { + let instructions = vec![0x03, 0x40, 0x0c, 0x00, 0x0b, 0x0b]; + module( + vec![func_type(&[], &[])], + Vec::new(), + vec![0], + 0, + false, + vec![function_body(instructions)], + Vec::new(), + ) + } + + fn stderr_exit_module(stderr: &[u8], code: i32) -> Vec { + let mut instructions = Vec::new(); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 64); + i32_store(&mut instructions); + i32_const(&mut instructions, 12); + i32_const(&mut instructions, stderr.len() as i32); + i32_store(&mut instructions); + i32_const(&mut instructions, 2); + i32_const(&mut instructions, 8); + i32_const(&mut instructions, 1); + i32_const(&mut instructions, 4); + call(&mut instructions, 0); + drop_value(&mut instructions); + i32_const(&mut instructions, code); + call(&mut instructions, 1); + end(&mut instructions); + + module( + vec![ + func_type(&[0x7f, 0x7f, 0x7f, 0x7f], &[0x7f]), + func_type(&[0x7f], &[]), + func_type(&[], &[]), + ], + vec![ + ("wasi_snapshot_preview1", "fd_write", 0), + ("wasi_snapshot_preview1", "proc_exit", 1), + ], + vec![2], + 2, + true, + vec![function_body(instructions)], + vec![(64, stderr.to_vec())], + ) + } + + fn unknown_import_module() -> Vec { + let mut instructions = Vec::new(); + call(&mut instructions, 0); + end(&mut instructions); + module( + vec![func_type(&[], &[])], + vec![("host", "missing", 0)], + vec![0], + 1, + false, + vec![function_body(instructions)], + Vec::new(), + ) + } + + #[test] + fn parse_tool_spec_accepts_valid_name_and_path() { + let (name, path) = parse_tool_spec(" greet = ./handler.wasm ").unwrap(); + assert_eq!(name, "greet"); + assert_eq!(path, PathBuf::from("./handler.wasm")); + } + + #[test] + fn parse_tool_spec_rejects_invalid_or_reserved_names() { + for spec in [ + "missing_equals", + "=handler.wasm", + "__hl_exit=handler.wasm", + "__dispatch=handler.wasm", + "fs_read=handler.wasm", + "net_socket=handler.wasm", + "greet=", + "greet= ", + ] { + assert!(parse_tool_spec(spec).is_err(), "{spec} should fail"); + } + } + + #[test] + fn cli_options_parse_wasi_dirs_env_and_limits() { + let rw = tempdir("rw"); + let ro = tempdir("ro"); + let opts = WasmToolOptions::from_cli( + &[format!("{}:/rw", rw.path().display())], + &[format!("{}:/ro", ro.path().display())], + &["A=B".to_string(), "EMPTY=".to_string()], + &[], + 123, + 456, + ) + .unwrap(); + + assert_eq!(opts.fuel, 123); + assert_eq!(opts.output_limit, 456); + assert_eq!( + opts.env, + vec![("A".into(), "B".into()), ("EMPTY".into(), "".into())] + ); + assert_eq!(opts.dirs.len(), 2); + assert_eq!(opts.dirs[0].host, fs::canonicalize(rw.path()).unwrap()); + assert_eq!(opts.dirs[0].guest, "/rw"); + assert!(!opts.dirs[0].read_only); + assert_eq!(opts.dirs[1].host, fs::canonicalize(ro.path()).unwrap()); + assert_eq!(opts.dirs[1].guest, "/ro"); + assert!(opts.dirs[1].read_only); + } + + #[test] + fn cli_options_default_wasi_guest_path_to_host() { + let dir = tempdir("default-guest"); + let opts = + WasmToolOptions::from_cli(&[dir.path().display().to_string()], &[], &[], &[], 1, 1) + .unwrap(); + assert_eq!(opts.dirs[0].guest, "/host"); + } + + #[test] + fn cli_options_use_last_explicit_env_value_for_duplicate_keys() { + let opts = WasmToolOptions::from_cli( + &[], + &[], + &[ + "A=first".to_string(), + "B=only".to_string(), + "A=second".to_string(), + ], + &[], + 1, + 1, + ) + .unwrap(); + assert_eq!( + opts.env, + vec![("A".into(), "second".into()), ("B".into(), "only".into())] + ); + } + + #[test] + fn cli_options_reject_invalid_values() { + let dir = tempdir("invalid-options"); + assert!(WasmToolOptions::from_cli(&[], &[], &[], &[], 0, 1).is_err()); + assert!(WasmToolOptions::from_cli(&[], &[], &[], &[], 1, 0).is_err()); + assert!( + WasmToolOptions::from_cli(&[], &[], &["NO_EQUALS".to_string()], &[], 1, 1).is_err() + ); + assert!(WasmToolOptions::from_cli(&[], &[], &["=value".to_string()], &[], 1, 1).is_err()); + assert!(WasmToolOptions::from_cli(&[], &[], &[], &["".to_string()], 1, 1).is_err()); + assert!(WasmToolOptions::from_cli( + &[format!("{}:relative", dir.path().display())], + &[], + &[], + &[], + 1, + 1, + ) + .is_err()); + assert!(WasmToolOptions::from_cli( + &[ + format!("{}:/dup", dir.path().display()), + format!("{}:/dup", dir.path().display()) + ], + &[], + &[], + &[], + 1, + 1, + ) + .is_err()); + } + + #[test] + fn has_capabilities_detects_any_wasi_capability_flag() { + assert!(!WasmToolOptions::has_capabilities(&[], &[], &[], &[])); + assert!(WasmToolOptions::has_capabilities( + &[".".into()], + &[], + &[], + &[] + )); + assert!(WasmToolOptions::has_capabilities( + &[], + &[".".into()], + &[], + &[] + )); + assert!(WasmToolOptions::has_capabilities( + &[], + &[], + &["A=B".into()], + &[] + )); + assert!(WasmToolOptions::has_capabilities( + &[], + &[], + &[], + &["PATH".into()] + )); + } + + #[test] + fn parse_tool_stdout_handles_raw_values_and_envelopes() { + assert_eq!(parse_tool_stdout("t", "").unwrap(), Value::Null); + assert_eq!(parse_tool_stdout("t", " \n ").unwrap(), Value::Null); + assert_eq!(parse_tool_stdout("t", "42").unwrap(), json!(42)); + assert_eq!( + parse_tool_stdout("t", "{\"result\":{\"ok\":true}}").unwrap(), + json!({"ok": true}) + ); + assert_eq!( + parse_tool_stdout("t", "{\"result\":1,\"extra\":2}").unwrap(), + json!({"result": 1, "extra": 2}) + ); + assert!( + err_string(parse_tool_stdout("t", "{\"error\":\"boom\"}").unwrap_err()) + .contains("Wasm tool t: boom") + ); + assert!(err_string(parse_tool_stdout("t", "not json").unwrap_err()) + .contains("wrote non-JSON stdout")); + } + + #[test] + fn load_all_rejects_duplicate_names_invalid_wasm_and_unknown_imports() { + let dir = tempdir("load-errors"); + let ok = dir.path().join("ok.wasm"); + let bad = dir.path().join("bad.wasm"); + let unknown = dir.path().join("unknown.wasm"); + fs::write(&ok, no_output_module()).unwrap(); + fs::write(&bad, b"not wasm").unwrap(); + fs::write(&unknown, unknown_import_module()).unwrap(); + let options = default_options(); + + let duplicate_err = WasmTool::load_all( + &[format!("a={}", ok.display()), format!("a={}", ok.display())], + &options, + ) + .err() + .expect("duplicate tool name should fail"); + assert!(err_string(duplicate_err).contains("duplicate --tool name")); + + let invalid_err = WasmTool::load_all(&[format!("bad={}", bad.display())], &options) + .err() + .expect("invalid wasm should fail"); + assert!(err_string(invalid_err).contains("compile Wasm tool")); + + let link_err = WasmTool::load_all(&[format!("unknown={}", unknown.display())], &options) + .err() + .expect("unknown import should fail"); + assert!(err_string(link_err).contains("link Wasm tool")); + } + + #[test] + fn invoke_passes_dispatch_request_on_stdin() { + let tool = load_tool("echo_req", stdin_echo_module(), default_options()); + let result = tool.invoke(json!({"n": 7, "s": "hello"})).unwrap(); + assert_eq!(result["name"], "echo_req"); + assert_eq!(result["args"], json!({"n": 7, "s": "hello"})); + } + + #[test] + fn invoke_passes_configured_environment() { + let key = "HL_WASM_JSON"; + let value = r#"{"result":"env-ok"}"#; + let options = + WasmToolOptions::from_cli(&[], &[], &[format!("{key}={value}")], &[], 1_000_000, 4096) + .unwrap(); + let tool = load_tool("env", env_value_module(key, value.len()), options); + let result = tool.invoke(json!({})).unwrap(); + assert_eq!(result, json!("env-ok")); + } + + #[test] + fn invoke_can_read_explicit_read_only_preopen() { + let root = tempdir("preopen-read"); + fs::write(root.path().join("answer.json"), br#"{"result":"file-ok"}"#).unwrap(); + let options = WasmToolOptions::from_cli( + &[], + &[format!("{}:.", root.path().display())], + &[], + &[], + 1_000_000, + 4096, + ) + .unwrap(); + let tool = load_tool( + "read_preopen", + preopen_read_file_module(b"answer.json"), + options, + ); + let result = tool.invoke(json!({})).unwrap(); + assert_eq!(result, json!("file-ok")); + } + + #[test] + fn invoke_unwraps_result_envelope() { + let tool = load_tool( + "answer", + stdout_module(br#"{"result":{"ok":true,"answer":42}}"#), + default_options(), + ); + let result = tool.invoke(json!({"ignored": true})).unwrap(); + assert_eq!(result, json!({"ok": true, "answer": 42})); + } + + #[test] + fn invoke_returns_null_for_empty_stdout() { + let tool = load_tool("empty", no_output_module(), default_options()); + let result = tool.invoke(json!({})).unwrap(); + assert_eq!(result, Value::Null); + } + + #[test] + fn invoke_converts_error_envelope_to_handler_error() { + let tool = load_tool( + "fail", + stdout_module(br#"{"error":"boom"}"#), + default_options(), + ); + let err = tool.invoke(json!({})).unwrap_err(); + assert!(err_string(err).contains("Wasm tool fail: boom")); + } + + #[test] + fn invoke_reports_nonzero_exit_status() { + let tool = load_tool("exit_only", stderr_exit_module(b"", 9), default_options()); + let err = tool.invoke(json!({})).unwrap_err(); + assert!(err_string(err).contains("exited with status 9")); + } + + #[test] + fn invoke_includes_stderr_for_nonzero_exit() { + let tool = load_tool( + "stderr_exit", + stderr_exit_module(b"details from stderr", 7), + default_options(), + ); + let err = tool.invoke(json!({})).unwrap_err(); + let msg = err_string(err); + assert!(msg.contains("exited with status 7")); + assert!(msg.contains("details from stderr")); + } + + #[test] + fn invoke_traps_when_stdout_exceeds_limit() { + let tool = load_tool( + "too_much", + stdout_module(br#"{"result":"this is too long"}"#), + options_with_limits(1_000_000, 8), + ); + let err = tool.invoke(json!({})).unwrap_err(); + let msg = err_string(err); + assert!(msg.contains("stdout may have reached output limit of 8 bytes")); + assert!(msg.contains("wrote non-JSON stdout")); + } + + #[test] + fn invoke_traps_when_fuel_is_exhausted() { + let tool = load_tool( + "spin", + infinite_loop_module(), + options_with_limits(10, 1024), + ); + let err = tool.invoke(json!({})).unwrap_err(); + let msg = err_string(err); + assert!(msg.contains("Wasm tool spin trapped")); + assert!(msg.contains("fuel")); + } +}