use std::path::PathBuf;
pub mod error;
pub use error::Error;
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;
#[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 struct Config {
pub kernel: PathBuf,
pub initramfs: PathBuf,
pub cmdline: String,
pub rootfs_share: Option<RootfsShare>,
pub rootfs_block: Option<RootfsBlock>,
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)>,
}
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_share: None,
rootfs_block: 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(),
}
}
pub fn set_rootfs_artifact(&mut self, artifact: RootfsArtifact, force_read_only: bool) {
match artifact {
RootfsArtifact::Directory { path, read_only } => {
self.rootfs_block = None;
self.rootfs_share = Some(RootfsShare {
path,
read_only: read_only || force_read_only,
});
}
RootfsArtifact::BlockImage { path, fstype } => {
self.rootfs_share = None;
self.rootfs_block = Some(RootfsBlock { path, fstype });
}
}
}
}
#[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());
}
}