pub mod birdcage;
pub mod firecracker;
pub mod libkrun;
pub mod process;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use tempfile::TempDir;
use tokio::process::Command;
use crate::{SandboxError, SandboxOpts};
const SCRATCH_SNAPSHOT_SUBDIR: &str = "workspace";
pub(crate) fn apply_snapshot_from(opts: &mut SandboxOpts) -> Result<Option<TempDir>, SandboxError> {
let Some(src) = opts.snapshot_from.take() else {
return Ok(None);
};
if !src.exists() {
return Err(SandboxError::Config(format!(
"snapshot_from source {} does not exist",
src.display()
)));
}
let tempdir = TempDir::new().map_err(SandboxError::Workspace)?;
let dst = tempdir.path().join(SCRATCH_SNAPSHOT_SUBDIR);
crate::workspace::snapshot(&src, &dst).map_err(SandboxError::Workspace)?;
opts.workspace = dst;
Ok(Some(tempdir))
}
pub(crate) fn which_on_path(bin: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(bin);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
pub(crate) fn build_command(opts: &SandboxOpts) -> io::Result<Command> {
let program = opts
.argv
.first()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "argv is empty"))?;
let mut cmd = Command::new(program);
cmd.args(opts.argv.iter().skip(1));
let cwd: &Path = opts.cwd.as_deref().unwrap_or(&opts.workspace);
cmd.current_dir(cwd);
cmd.env_clear();
for (k, v) in &opts.env {
cmd.env(k, v);
}
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
cmd.kill_on_drop(true);
Ok(cmd)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn apply_snapshot_from_none_is_passthrough() {
let scratch = tempdir().unwrap();
let workspace = scratch.path().join("ws");
fs::create_dir(&workspace).unwrap();
let mut opts = SandboxOpts::new(workspace.clone(), vec!["/bin/true".into()]);
let handle = apply_snapshot_from(&mut opts).expect("ok");
assert!(handle.is_none(), "no snapshot requested, no tempdir handed back");
assert_eq!(opts.workspace, workspace, "workspace untouched");
}
#[test]
fn apply_snapshot_from_materialises_and_overrides_workspace() {
let scratch = tempdir().unwrap();
let src = scratch.path().join("src");
fs::create_dir(&src).unwrap();
fs::write(src.join("sentinel.txt"), b"hello").unwrap();
let mut opts = SandboxOpts::new(PathBuf::from("/unused"), vec!["/bin/true".into()])
.with_snapshot_from(src.clone());
let handle = apply_snapshot_from(&mut opts).expect("snapshot ok");
let snapshot_dir = handle.as_ref().expect("tempdir handed back").path().to_path_buf();
assert_eq!(opts.workspace, snapshot_dir.join("workspace"));
assert!(
opts.workspace.join("sentinel.txt").is_file(),
"snapshot must reproduce the source's sentinel"
);
assert!(opts.snapshot_from.is_none());
drop(handle);
assert!(!snapshot_dir.exists(), "tempdir must remove snapshot on drop");
}
#[test]
fn apply_snapshot_from_refuses_missing_source() {
let mut opts = SandboxOpts::new(PathBuf::from("/unused"), vec!["/bin/true".into()])
.with_snapshot_from(PathBuf::from("/nyx-snapshot-from-does-not-exist"));
match apply_snapshot_from(&mut opts) {
Err(SandboxError::Config(reason)) => {
assert!(reason.contains("snapshot_from"), "reason names the field: {reason}");
}
other => panic!("expected Config error, got {other:?}"),
}
}
}