use std::path::PathBuf;
pub mod error;
pub use error::Error;
mod boot;
mod cmdline;
mod lifecycle;
mod session;
mod terminal;
mod vz;
pub mod desktop;
pub mod ffi;
pub mod provider;
pub use desktop::{Action, ResponseHeader, ScrollDirection};
pub use lifecycle::{run, RunOutput};
pub use provider::{BlockFs, RootfsArtifact};
pub use session::{Session, SessionClient, SessionEnd, StopHandle};
pub use vmette_proto::ShareMount;
pub(crate) const ROOTFS_SHARE_TAG: &str = "rootfs";
pub(crate) const CTL_SHARE_TAG: &str = "ctl";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WorkloadStrategy {
#[default]
OneShot,
Agent,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum VsockPort {
Disabled,
#[default]
Auto,
Fixed(u32),
}
#[derive(Debug, Clone)]
pub struct RootfsShare {
pub path: PathBuf,
pub read_only: bool,
}
#[derive(Debug, Clone)]
pub struct RootfsBlock {
pub path: PathBuf,
pub fstype: BlockFs,
}
#[derive(Debug, Clone)]
pub enum Rootfs {
Share(RootfsShare),
Block(RootfsBlock),
}
#[derive(Debug, Clone)]
pub struct Config {
pub kernel: PathBuf,
pub initramfs: PathBuf,
pub cmdline: String,
pub rootfs: Option<Rootfs>,
pub shares: Vec<ShareMount>,
pub disks: Vec<PathBuf>,
pub exec_cmd: Option<String>,
pub switch_root: bool,
pub net: bool,
pub vsock_port: VsockPort,
pub guest_vsock_port: u32,
pub timeout_seconds: Option<u32>,
pub vcpus: u8,
pub mem_mib: u64,
pub build_snapshot: Option<PathBuf>,
pub resume_snapshot: Option<PathBuf>,
pub workload: WorkloadStrategy,
pub display_size: (u32, u32),
pub quiet: bool,
pub env: Vec<(String, String)>,
pub capture_output: bool,
pub scratch_mib: Option<u64>,
}
impl Config {
pub fn new(kernel: impl Into<PathBuf>, initramfs: impl Into<PathBuf>) -> Self {
Self {
kernel: kernel.into(),
initramfs: initramfs.into(),
cmdline: "console=hvc0 quiet".into(),
rootfs: None,
shares: Vec::new(),
disks: Vec::new(),
exec_cmd: None,
switch_root: false,
net: false,
vsock_port: VsockPort::Auto,
guest_vsock_port: 1025,
timeout_seconds: None,
vcpus: 1,
mem_mib: 512,
build_snapshot: None,
resume_snapshot: None,
workload: WorkloadStrategy::OneShot,
display_size: (1280, 800),
quiet: false,
env: Vec::new(),
capture_output: false,
scratch_mib: None,
}
}
pub fn set_rootfs_artifact(&mut self, artifact: RootfsArtifact, force_read_only: bool) {
match artifact {
RootfsArtifact::Directory {
path,
read_only,
image_env,
} => {
self.rootfs = Some(Rootfs::Share(RootfsShare {
path,
read_only: read_only || force_read_only,
}));
if !image_env.is_empty() {
let caller = std::mem::take(&mut self.env);
self.env = image_env;
self.env.extend(caller);
}
}
RootfsArtifact::BlockImage { path, fstype } => {
self.rootfs = Some(Rootfs::Block(RootfsBlock { path, fstype }));
}
}
}
pub fn from_run_request(
req: &vmette_proto::daemon::Request,
artifact: RootfsArtifact,
capture_output: bool,
) -> Self {
let mut c = Config::new(&req.kernel, &req.initramfs);
c.exec_cmd = Some(req.exec.clone());
c.shares = req.shares.clone();
c.disks = req.disks.clone();
c.net = req.net;
c.switch_root = req.switch_root;
c.capture_output = capture_output;
c.timeout_seconds = req.timeout_seconds;
c.scratch_mib = req.scratch_mib;
if let Some(v) = req.vcpus {
c.vcpus = v;
}
if let Some(m) = req.mem_mib {
c.mem_mib = m;
}
if let Some(g) = req.guest_vsock_port {
c.guest_vsock_port = g;
}
c.vsock_port = match req.vsock_port {
Some(-1) => VsockPort::Disabled,
Some(n) if n > 0 => VsockPort::Fixed(n as u32),
_ => VsockPort::Auto,
};
c.set_rootfs_artifact(artifact, req.rootfs_ro);
c
}
}
#[doc(hidden)]
pub fn render_env_exports(pairs: &[(String, String)]) -> Option<String> {
let mut out = String::new();
for (key, val) in pairs {
if !is_valid_env_key(key) {
continue;
}
let escaped = val.replace('\'', "'\\''");
out.push_str("export ");
out.push_str(key);
out.push_str("='");
out.push_str(&escaped);
out.push_str("'\n");
}
(!out.is_empty()).then_some(out)
}
#[doc(hidden)]
pub fn is_valid_env_key(key: &str) -> bool {
let mut bytes = key.bytes();
matches!(bytes.next(), Some(c) if c.is_ascii_alphabetic() || c == b'_')
&& bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
}
#[cfg(test)]
mod env_tests {
use super::{is_valid_env_key, render_env_exports};
#[test]
fn valid_env_keys() {
assert!(is_valid_env_key("PATH"));
assert!(is_valid_env_key("_x"));
assert!(is_valid_env_key("A1_B2"));
assert!(!is_valid_env_key("")); assert!(!is_valid_env_key("1LEAD")); assert!(!is_valid_env_key("FOO-BAR")); assert!(!is_valid_env_key("FOO BAR")); assert!(!is_valid_env_key("a=b")); }
#[test]
fn render_escapes_and_skips_invalid() {
let pairs = vec![
("PATH".into(), "/a:/b".into()),
("WEIRD".into(), "it's".into()),
("HAS".into(), "a=b".into()), ("BAD-KEY".into(), "x".into()), ];
let out = render_env_exports(&pairs).expect("some env");
assert!(out.contains("export PATH='/a:/b'\n"));
assert!(out.contains("export HAS='a=b'\n"));
assert!(out.contains(r"export WEIRD='it'\''s'"));
assert!(!out.contains("BAD-KEY"));
assert!(render_env_exports(&[("1BAD".into(), "x".into())]).is_none());
assert!(render_env_exports(&[]).is_none());
}
#[test]
fn set_rootfs_artifact_prepends_image_env_caller_overrides() {
use crate::{Config, RootfsArtifact};
let mut c = Config::new("/k", "/i");
c.env = vec![
("PATH".into(), "/caller".into()),
("CALLER_ONLY".into(), "1".into()),
];
c.set_rootfs_artifact(
RootfsArtifact::Directory {
path: "/r".into(),
read_only: false,
image_env: vec![
("PATH".into(), "/image".into()),
("IMAGE_ONLY".into(), "1".into()),
],
},
false,
);
assert_eq!(
c.env,
vec![
("PATH".into(), "/image".into()),
("IMAGE_ONLY".into(), "1".into()),
("PATH".into(), "/caller".into()),
("CALLER_ONLY".into(), "1".into()),
]
);
let rendered = render_env_exports(&c.env).unwrap();
let img = rendered.find("export PATH='/image'").unwrap();
let cal = rendered.find("export PATH='/caller'").unwrap();
assert!(cal > img, "caller PATH must render after image PATH");
}
#[test]
fn set_rootfs_artifact_no_image_env_leaves_caller_env() {
use crate::{Config, RootfsArtifact};
let mut c = Config::new("/k", "/i");
c.env = vec![("FOO".into(), "bar".into())];
c.set_rootfs_artifact(
RootfsArtifact::Directory {
path: "/r".into(),
read_only: false,
image_env: Vec::new(),
},
false,
);
assert_eq!(c.env, vec![("FOO".into(), "bar".into())]);
}
}