use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use sha2::{Digest, Sha256};
use super::guest_check::validate_guest_bytes;
use crate::util::find_binary;
const ID_LEN: usize = 12;
pub struct GuestBinary {
path: PathBuf,
id: String,
}
impl GuestBinary {
pub fn get() -> BoxliteResult<&'static Self> {
static INSTANCE: OnceLock<GuestBinary> = OnceLock::new();
if let Some(binary) = INSTANCE.get() {
return Ok(binary);
}
let resolved = Self::resolve()?;
Ok(INSTANCE.get_or_init(|| resolved))
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn id(&self) -> &str {
&self.id
}
fn resolve() -> BoxliteResult<Self> {
Self::resolve_at(find_binary("boxlite-guest")?)
}
pub(crate) fn resolve_at(path: PathBuf) -> BoxliteResult<Self> {
let started = std::time::Instant::now();
let bytes = std::fs::read(&path).map_err(|e| {
BoxliteError::Storage(format!(
"Cannot read guest binary {}: {}",
path.display(),
e
))
})?;
validate_guest_bytes(&bytes, &path)?;
let id = hex::encode(Sha256::digest(&bytes))[..ID_LEN].to_string();
tracing::info!(
path = %path.display(),
id = %id,
size_mb = bytes.len() / (1024 * 1024),
elapsed_ms = started.elapsed().as_millis() as u64,
"Resolved guest binary"
);
Ok(Self { path, id })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_guest(filler: u8) -> Vec<u8> {
let mut elf = vec![0u8; 128];
elf[..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
elf[4] = 2; let machine: u16 = match std::env::consts::ARCH {
"x86_64" => 0x3E,
_ => 0xB7,
};
elf[18..20].copy_from_slice(&machine.to_le_bytes());
elf[64..].fill(filler); elf
}
fn write_guest(dir: &tempfile::TempDir, filler: u8) -> PathBuf {
let path = dir.path().join("boxlite-guest");
std::fs::write(&path, fake_guest(filler)).unwrap();
path
}
#[test]
fn id_tracks_the_bytes_on_disk() {
let dir = tempfile::tempdir().unwrap();
let before = GuestBinary::resolve_at(write_guest(&dir, 0xAA)).unwrap();
let first_id = before.id().to_string();
let after = GuestBinary::resolve_at(write_guest(&dir, 0xBB)).unwrap();
assert_ne!(
first_id,
after.id(),
"a rebuilt guest binary must not keep the previous identity"
);
assert_eq!(before.path(), after.path(), "same path, different contents");
}
#[test]
fn id_is_stable_for_unchanged_bytes() {
let dir = tempfile::tempdir().unwrap();
let path = write_guest(&dir, 0xAA);
let first = GuestBinary::resolve_at(path.clone()).unwrap();
let second = GuestBinary::resolve_at(path).unwrap();
assert_eq!(
first.id(),
second.id(),
"identical bytes must reuse the cached rootfs, not rebuild it"
);
assert_eq!(first.id().len(), ID_LEN);
}
#[test]
fn a_missing_binary_is_reported_by_path() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("boxlite-guest");
let error = GuestBinary::resolve_at(missing.clone())
.err()
.expect("a missing binary must not resolve");
assert!(
error.to_string().contains("Cannot read")
&& error.to_string().contains("boxlite-guest"),
"error should name the unreadable path: {error}"
);
}
#[test]
fn a_non_elf_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("boxlite-guest");
std::fs::write(&path, vec![0u8; 128]).unwrap();
let error = GuestBinary::resolve_at(path)
.err()
.expect("bad magic must not resolve");
assert!(
error.to_string().contains("ELF"),
"error should name the problem: {error}"
);
}
}