Gunnar

nordisk/heimdall

crates/heimdall-wasm/tests/rust_guest.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! A real Rust guest built with wit-bindgen for wasm32 against the published
//! WIT runs in the host: the interface is usable by guest authors, not only
//! by hand-written WAT. Heimdall is a defensive sandbox.

mod common;
use common::*;
use heimdall_wasm::{codes, RunLimits, Verdict};
use std::path::Path;
use std::sync::OnceLock;
use std::time::Duration;

/// Build the guest once (wasm32-unknown-unknown, release) and componentize it.
fn rust_guest() -> &'static [u8] {
    static BYTES: OnceLock<Vec<u8>> = OnceLock::new();
    BYTES.get_or_init(|| {
        let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/rust-guest/Cargo.toml");
        let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("rust-guest");
        let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
        let st = std::process::Command::new(cargo)
            .args([
                "build",
                "--release",
                "--target",
                "wasm32-unknown-unknown",
                "--manifest-path",
            ])
            .arg(&manifest)
            .arg("--target-dir")
            .arg(&target)
            .env_remove("RUSTFLAGS")
            .env_remove("CARGO_ENCODED_RUSTFLAGS")
            .status()
            .expect("cargo could not be started");
        assert!(st.success(), "building the Rust test guest failed");
        let core =
            std::fs::read(target.join("wasm32-unknown-unknown/release/heimdall_test_guest.wasm"))
                .unwrap();
        wit_component::ComponentEncoder::default()
            .module(&core)
            .unwrap()
            .validate(true)
            .encode()
            .unwrap()
    })
}

#[test]
fn a_rust_guest_accepts_and_emits_its_files() {
    let kit = Kit::new("rust-good");
    let g = kit.host().load(&kit.place("rust", rust_guest())).unwrap();
    let o = g.run(
        b"hello",
        &params(&[("b", "2"), ("a", "1")]),
        &RunLimits::default(),
    );
    assert_eq!(
        o.verdict,
        Verdict::Accepted {
            note: "accepted 5 bytes".into()
        }
    );
    let names: Vec<&str> = o.files.iter().map(|f| f.name.as_str()).collect();
    assert_eq!(names, ["echo.bin", "params.txt"]);
    assert_eq!(o.files[0].bytes, b"hello");
    assert_eq!(o.files[1].bytes, b"a=1\nb=2");
}

#[test]
fn a_rust_guest_refusal_is_passed_on() {
    let kit = Kit::new("rust-refuse");
    let g = kit.host().load(&kit.place("rust", rust_guest())).unwrap();
    assert_eq!(
        g.run(b"Tx", &none(), &RunLimits::default()).verdict,
        Verdict::Refused {
            code: codes::GUEST_REFUSED,
            reason: "tampered input".into()
        }
    );
}

#[test]
fn a_rust_guest_panic_is_a_trap_not_a_host_crash() {
    let kit = Kit::new("rust-panic");
    let g = kit.host().load(&kit.place("rust", rust_guest())).unwrap();
    assert_trapped(&g.run(b"M", &none(), &RunLimits::default()), "unreachable");
}

#[test]
fn a_rust_guest_loop_is_stopped_by_fuel_and_by_the_clock() {
    let kit = Kit::new("rust-loop");
    let g = kit.host().load(&kit.place("rust", rust_guest())).unwrap();
    assert_trapped(
        &g.run(
            b"L",
            &none(),
            &RunLimits {
                fuel: 10_000_000,
                ..RunLimits::default()
            },
        ),
        "out of fuel",
    );
    let clock = RunLimits {
        fuel: u64::MAX,
        timeout: Duration::from_millis(200),
        ..RunLimits::default()
    };
    assert_trapped(&g.run(b"L", &none(), &clock), "wall-clock timeout");
}

#[test]
fn a_rust_guest_imports_nothing_but_emit_file() {
    // Loading proves it (anything else is refused at link); this names the claim.
    let kit = Kit::new("rust-imports");
    assert!(kit.host().load(&kit.place("rust", rust_guest())).is_ok());
}