harn-vm 0.10.165

Async bytecode virtual machine for the Harn programming language
Documentation
use super::HostRequestTrace;
use crate::{Vm, VmValue};
use std::sync::{Arc, Mutex};

#[derive(Default)]
struct RecordingBridge(Mutex<Vec<HostRequestTrace>>);

impl crate::HostCallBridge for RecordingBridge {
    fn dispatch<'a>(
        &'a self,
        _capability: &'a str,
        _operation: &'a str,
        _params: &'a crate::value::DictMap,
    ) -> crate::HostCallDispatchFuture<'a> {
        panic!("VM host calls must carry attribution")
    }

    fn dispatch_traced<'a>(
        &'a self,
        capability: &'a str,
        operation: &'a str,
        _params: &'a crate::value::DictMap,
        trace: &'a HostRequestTrace,
    ) -> crate::HostCallDispatchFuture<'a> {
        assert_eq!((capability, operation), ("env", "scan"));
        self.0.lock().unwrap().push(trace.clone());
        crate::host_call_ready(Ok(Some(VmValue::Nil)))
    }
}

#[tokio::test(flavor = "current_thread")]
async fn imported_callers_and_repeated_shapes_survive_async_dispatch_without_values() {
    let temp = tempfile::tempdir().unwrap();
    let helper = temp.path().join("helper.harn");
    let entry = temp.path().join("entry.harn");
    std::fs::write(
        &helper,
        r#"pub fn repeated() {
            for item in [1, 2] {
                host_call("env.scan", {path: "private-path-sentinel", argv: ["secret-command", "secret-argument"]})
            }
        }
        pub fn distinct() {
            host_call("env.scan", {path: "another-private-path", argv: []})
        }"#,
    ).unwrap();
    std::fs::write(
        &entry,
        r#"import { repeated, distinct } from "./helper"
        pub fn route() { repeated(); distinct() }"#,
    )
    .unwrap();
    let bridge = Arc::new(RecordingBridge::default());
    let _guard = crate::install_host_call_bridge(bridge.clone());
    let mut vm = Vm::new();
    crate::stdlib::register_vm_stdlib(&mut vm);
    vm.enable_trusted_host_dispatch().unwrap();
    let exports = vm.load_module_exports(&entry).await.unwrap();
    vm.call_closure_pub(exports.get("route").unwrap(), &[])
        .await
        .unwrap();

    let traces = bridge.0.lock().unwrap();
    assert_eq!(traces.len(), 3);
    assert_eq!(traces[0], traces[1]);
    for (trace, function, length) in [(&traces[0], "repeated", 2), (&traces[2], "distinct", 0)] {
        let caller = trace.caller.as_ref().expect("measured caller");
        assert_eq!(caller.function, function);
        assert_eq!(
            std::path::Path::new(caller.module.as_deref().unwrap())
                .canonicalize()
                .unwrap(),
            helper.canonicalize().unwrap(),
        );
        assert_eq!(trace.arguments.len(), 2);
        assert_eq!(trace.arguments["path"].value_type, "string");
        assert_eq!(trace.arguments["path"].list_length, None);
        assert_eq!(trace.arguments["argv"].value_type, "list");
        assert_eq!(trace.arguments["argv"].list_length, Some(length));
    }
    let serialized = serde_json::to_string(&*traces).unwrap();
    for secret in [
        "private-path-sentinel",
        "secret-command",
        "secret-argument",
        "another-private-path",
    ] {
        assert!(
            !serialized.contains(secret),
            "argument value leaked: {serialized}"
        );
    }
}