use std::io;
use std::path::{Path, PathBuf};
const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6D];
pub const WASM_LAYER_MEDIA_TYPES: &[&str] = &[
"application/vnd.bytecodealliance.wasm.component.layer.v0+wasm",
"application/vnd.wasm.content.layer.v1+wasm",
"application/wasm",
];
pub fn is_wasm_media_type(media_type: &str) -> bool {
WASM_LAYER_MEDIA_TYPES.contains(&media_type)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WasmRuntime {
Wasmtime,
WasmEdge,
#[default]
Auto,
}
#[derive(Debug, Clone, Default)]
pub struct WasiConfig {
pub runtime: WasmRuntime,
pub env: Vec<(String, String)>,
pub preopened_dirs: Vec<(PathBuf, PathBuf)>,
}
const WASM_MODULE_VERSION: [u8; 4] = [0x01, 0x00, 0x00, 0x00];
pub fn is_wasm_binary(path: &Path) -> io::Result<bool> {
use std::io::Read;
let mut f = match std::fs::File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(e),
};
let mut magic = [0u8; 4];
match f.read_exact(&mut magic) {
Ok(()) => Ok(magic == WASM_MAGIC),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(false),
Err(e) => Err(e),
}
}
pub fn is_wasm_component_binary(path: &Path) -> io::Result<bool> {
use std::io::Read;
let mut f = match std::fs::File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(e),
};
let mut header = [0u8; 8];
match f.read_exact(&mut header) {
Ok(()) => {
if header[..4] != WASM_MAGIC {
return Ok(false);
}
Ok(header[4..8] != WASM_MODULE_VERSION)
}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(false),
Err(e) => Err(e),
}
}
pub fn find_wasm_runtime(preferred: WasmRuntime) -> Option<(WasmRuntime, PathBuf)> {
let candidates: &[(&str, WasmRuntime)] = match preferred {
WasmRuntime::WasmEdge => &[
("wasmedge", WasmRuntime::WasmEdge),
("wasmtime", WasmRuntime::Wasmtime),
],
_ => &[
("wasmtime", WasmRuntime::Wasmtime),
("wasmedge", WasmRuntime::WasmEdge),
],
};
for (name, rt) in candidates {
if let Some(path) = find_in_path(name) {
return Some((*rt, path));
}
}
None
}
fn find_in_path(name: &str) -> Option<PathBuf> {
let path_env = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_env) {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
pub fn spawn_wasm(
program: &Path,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
stdin: std::process::Stdio,
stdout: std::process::Stdio,
stderr: std::process::Stdio,
) -> io::Result<std::process::Child> {
let (rt, runtime_bin) = find_wasm_runtime(wasi.runtime).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"no Wasm runtime found in PATH — install wasmtime or wasmedge",
)
})?;
log::info!(
"spawning Wasm module '{}' via {:?} ({})",
program.display(),
rt,
runtime_bin.display()
);
let mut cmd = match rt {
WasmRuntime::Wasmtime => build_wasmtime_cmd(&runtime_bin, program, extra_args, wasi),
WasmRuntime::WasmEdge => build_wasmedge_cmd(&runtime_bin, program, extra_args, wasi),
WasmRuntime::Auto => unreachable!("Auto resolved to a concrete runtime above"),
};
cmd.stdin(stdin).stdout(stdout).stderr(stderr).spawn()
}
fn build_wasmtime_cmd(
runtime: &Path,
wasm: &Path,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
) -> std::process::Command {
let mut cmd = std::process::Command::new(runtime);
cmd.arg("run");
for (host, guest) in &wasi.preopened_dirs {
cmd.arg("--dir")
.arg(format!("{}::{}", host.display(), guest.display()));
}
for (k, v) in &wasi.env {
cmd.arg("--env").arg(format!("{k}={v}"));
}
cmd.arg("--").arg(wasm);
cmd.args(extra_args);
cmd
}
fn build_wasmedge_cmd(
runtime: &Path,
wasm: &Path,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
) -> std::process::Command {
let mut cmd = std::process::Command::new(runtime);
for (host, guest) in &wasi.preopened_dirs {
cmd.arg("--dir")
.arg(format!("{}:{}", host.display(), guest.display()));
}
for (k, v) in &wasi.env {
cmd.arg("--env").arg(format!("{k}={v}"));
}
cmd.arg(wasm);
cmd.args(extra_args);
cmd
}
#[cfg(feature = "embedded-wasm")]
pub fn run_wasm_embedded(
program: &Path,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
) -> i32 {
match run_embedded_inner(program, extra_args, wasi) {
Ok(code) => code,
Err(e) => {
log::error!("embedded wasm: {}", e);
1
}
}
}
#[cfg(feature = "embedded-wasm")]
fn run_embedded_inner(
program: &Path,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
if is_wasm_component_binary(program).unwrap_or(false) {
log::info!(
"embedded wasm: '{}' is a component — using P2 path",
program.display()
);
run_embedded_component_file(program, extra_args, wasi)
} else {
use wasmtime::{Engine, Module};
let engine = Engine::default();
let module = Module::from_file(&engine, program)?;
run_embedded_module(&engine, &module, extra_args, wasi)
}
}
#[cfg(feature = "embedded-wasm")]
fn run_embedded_component_file(
program: &Path,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
use wasmtime::component::Component;
use wasmtime::{Config, Engine};
let mut config = Config::new();
config.wasm_component_model(true);
let engine = Engine::new(&config)?;
let component = Component::from_file(&engine, program)?;
run_embedded_component(&engine, &component, extra_args, wasi)
}
#[cfg(feature = "embedded-wasm")]
pub fn run_embedded_module(
engine: &wasmtime::Engine,
module: &wasmtime::Module,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
use wasmtime::{Linker, Store};
use wasmtime_wasi::p1::{self, WasiP1Ctx};
use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder};
let mut builder = WasiCtxBuilder::new();
builder.inherit_stdin().inherit_stdout().inherit_stderr();
for (k, v) in &wasi.env {
builder.env(k, v);
}
for (host, guest) in &wasi.preopened_dirs {
builder.preopened_dir(
host,
guest.to_string_lossy(),
DirPerms::all(),
FilePerms::all(),
)?;
}
builder.arg("module.wasm");
for arg in extra_args {
builder.arg(arg.to_string_lossy());
}
let wasi_ctx: WasiP1Ctx = builder.build_p1();
let mut store = Store::new(engine, wasi_ctx);
let mut linker: Linker<WasiP1Ctx> = Linker::new(engine);
p1::add_to_linker_sync(&mut linker, |s| s)?;
let instance = linker.instantiate(&mut store, module)?;
let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
match start.call(&mut store, ()) {
Ok(()) => Ok(0),
Err(e) => {
if let Some(exit) = e
.chain()
.find_map(|ce| ce.downcast_ref::<wasmtime_wasi::I32Exit>())
{
Ok(exit.0)
} else {
Err(e.into())
}
}
}
}
#[cfg(feature = "embedded-wasm")]
pub fn run_embedded_component(
engine: &wasmtime::Engine,
component: &wasmtime::component::Component,
extra_args: &[std::ffi::OsString],
wasi: &WasiConfig,
) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
use wasmtime::component::Linker;
use wasmtime::Store;
use wasmtime_wasi::{DirPerms, FilePerms, ResourceTable, WasiCtx, WasiCtxBuilder, WasiView};
struct WasiState {
ctx: WasiCtx,
table: ResourceTable,
}
impl WasiView for WasiState {
fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
wasmtime_wasi::WasiCtxView {
ctx: &mut self.ctx,
table: &mut self.table,
}
}
}
let mut builder = WasiCtxBuilder::new();
builder.inherit_stdin().inherit_stdout().inherit_stderr();
builder.arg("module.wasm");
for arg in extra_args {
builder.arg(arg.to_string_lossy());
}
for (k, v) in &wasi.env {
builder.env(k, v);
}
for (host, guest) in &wasi.preopened_dirs {
builder.preopened_dir(
host,
guest.to_string_lossy(),
DirPerms::all(),
FilePerms::all(),
)?;
}
let state = WasiState {
ctx: builder.build(),
table: ResourceTable::new(),
};
let mut store = Store::new(engine, state);
let mut linker: Linker<WasiState> = Linker::new(engine);
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
let command =
wasmtime_wasi::p2::bindings::sync::Command::instantiate(&mut store, component, &linker)?;
match command.wasi_cli_run().call_run(&mut store) {
Ok(Ok(())) => Ok(0),
Ok(Err(())) => Ok(1),
Err(e) => {
if let Some(exit) = e
.chain()
.find_map(|ce| ce.downcast_ref::<wasmtime_wasi::I32Exit>())
{
Ok(exit.0)
} else {
Err(e.into())
}
}
}
}
#[cfg(all(test, feature = "embedded-wasm"))]
mod embedded_tests {
use super::*;
use wasmtime::{Engine, Module};
fn run_wat(wat: &str) -> i32 {
let engine = Engine::default();
let module = Module::new(&engine, wat.as_bytes()).unwrap();
run_embedded_module(&engine, &module, &[], &WasiConfig::default()).unwrap()
}
const WAT_EXIT_0: &str = r#"(module
(import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
(memory 1)
(export "memory" (memory 0))
(func $_start i32.const 0 call $proc_exit)
(export "_start" (func $_start)))"#;
const WAT_EXIT_42: &str = r#"(module
(import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
(memory 1)
(export "memory" (memory 0))
(func $_start i32.const 42 call $proc_exit)
(export "_start" (func $_start)))"#;
#[test]
fn test_embedded_exit_zero() {
assert_eq!(run_wat(WAT_EXIT_0), 0);
}
#[test]
fn test_embedded_exit_nonzero() {
assert_eq!(run_wat(WAT_EXIT_42), 42);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_is_wasm_binary_magic_bytes() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&[0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00])
.unwrap();
tmp.flush().unwrap();
assert!(is_wasm_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_binary_elf_is_false() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(b"\x7fELF\x02\x01\x01\x00").unwrap();
tmp.flush().unwrap();
assert!(!is_wasm_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_binary_too_short() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(b"\x00\x61").unwrap();
tmp.flush().unwrap();
assert!(!is_wasm_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_binary_empty_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
assert!(!is_wasm_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_binary_missing_path() {
let result = is_wasm_binary(Path::new("/tmp/__pelagos_nonexistent_abc123.wasm"));
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_is_wasm_component_binary_module_is_false() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&[0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00])
.unwrap();
tmp.flush().unwrap();
assert!(!is_wasm_component_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_component_binary_component_is_true() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&[0x00, 0x61, 0x73, 0x6D, 0x0d, 0x00, 0x01, 0x00])
.unwrap();
tmp.flush().unwrap();
assert!(is_wasm_component_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_component_binary_too_short_is_false() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&[0x00, 0x61, 0x73, 0x6D]).unwrap(); tmp.flush().unwrap();
assert!(!is_wasm_component_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_component_binary_non_wasm_is_false() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(b"\x7fELF\x02\x01\x01\x00").unwrap();
tmp.flush().unwrap();
assert!(!is_wasm_component_binary(tmp.path()).unwrap());
}
#[test]
fn test_is_wasm_media_type_known_types() {
assert!(is_wasm_media_type(
"application/vnd.bytecodealliance.wasm.component.layer.v0+wasm"
));
assert!(is_wasm_media_type(
"application/vnd.wasm.content.layer.v1+wasm"
));
assert!(is_wasm_media_type("application/wasm"));
}
#[test]
fn test_is_wasm_media_type_standard_layer_is_false() {
assert!(!is_wasm_media_type(
"application/vnd.oci.image.layer.v1.tar+gzip"
));
assert!(!is_wasm_media_type(
"application/vnd.docker.image.rootfs.diff.tar.gzip"
));
assert!(!is_wasm_media_type(""));
}
#[test]
fn test_find_wasm_runtime_does_not_panic() {
let _ = find_wasm_runtime(WasmRuntime::Auto);
let _ = find_wasm_runtime(WasmRuntime::Wasmtime);
let _ = find_wasm_runtime(WasmRuntime::WasmEdge);
}
fn args_of(cmd: &std::process::Command) -> Vec<String> {
cmd.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect()
}
#[test]
fn test_wasmtime_cmd_identity_dir_mapping() {
let wasi = WasiConfig {
runtime: WasmRuntime::Wasmtime,
env: vec![],
preopened_dirs: vec![(PathBuf::from("/data"), PathBuf::from("/data"))],
};
let cmd = build_wasmtime_cmd(Path::new("wasmtime"), Path::new("app.wasm"), &[], &wasi);
let args = args_of(&cmd);
assert!(
args.iter().any(|a| a == "/data::/data"),
"expected --dir /data::/data in args: {args:?}"
);
}
#[test]
fn test_wasmtime_cmd_mapped_dir() {
let wasi = WasiConfig {
runtime: WasmRuntime::Wasmtime,
env: vec![],
preopened_dirs: vec![(PathBuf::from("/host/binddata"), PathBuf::from("/data"))],
};
let cmd = build_wasmtime_cmd(Path::new("wasmtime"), Path::new("app.wasm"), &[], &wasi);
let args = args_of(&cmd);
assert!(
args.iter().any(|a| a == "/host/binddata::/data"),
"expected --dir /host/binddata::/data in args: {args:?}"
);
assert!(
!args.iter().any(|a| a == "/host/binddata::/host/binddata"),
"regression: produced identity mapping --dir /host/binddata::/host/binddata"
);
}
#[test]
fn test_wasmedge_cmd_mapped_dir() {
let wasi = WasiConfig {
runtime: WasmRuntime::WasmEdge,
env: vec![],
preopened_dirs: vec![(PathBuf::from("/host/binddata"), PathBuf::from("/data"))],
};
let cmd = build_wasmedge_cmd(Path::new("wasmedge"), Path::new("app.wasm"), &[], &wasi);
let args = args_of(&cmd);
assert!(
args.iter().any(|a| a == "/host/binddata:/data"),
"expected --dir /host/binddata:/data in args: {args:?}"
);
assert!(
!args.iter().any(|a| a == "/host/binddata:/host/binddata"),
"regression: produced identity mapping --dir /host/binddata:/host/binddata"
);
}
#[test]
fn test_wasmtime_cmd_env_vars() {
let wasi = WasiConfig {
runtime: WasmRuntime::Wasmtime,
env: vec![("FOO".into(), "bar".into()), ("BAZ".into(), "qux".into())],
preopened_dirs: vec![],
};
let cmd = build_wasmtime_cmd(Path::new("wasmtime"), Path::new("app.wasm"), &[], &wasi);
let args = args_of(&cmd);
assert!(
args.iter().any(|a| a == "FOO=bar"),
"expected FOO=bar in args: {args:?}"
);
assert!(
args.iter().any(|a| a == "BAZ=qux"),
"expected BAZ=qux in args: {args:?}"
);
}
}