use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use sha2::{Digest, Sha256};
use super::guest_binary::GuestBinary;
use super::guest_check::validate_guest_bytes;
use crate::util::find_binary;
const ID_LEN: usize = 12;
const HOST_GUEST_MKE2FS: &str = "guest-mke2fs";
const HOST_GUEST_RESIZE2FS: &str = "guest-resize2fs";
pub struct GuestTool {
path: PathBuf,
}
impl GuestTool {
pub fn path(&self) -> &Path {
&self.path
}
}
pub struct GuestArtifacts {
guest_path: PathBuf,
mke2fs: GuestTool,
resize2fs: GuestTool,
combined_id: String,
}
impl GuestArtifacts {
pub fn get() -> BoxliteResult<&'static Self> {
static INSTANCE: OnceLock<GuestArtifacts> = OnceLock::new();
if let Some(artifacts) = INSTANCE.get() {
return Ok(artifacts);
}
let resolved = Self::resolve()?;
Ok(INSTANCE.get_or_init(|| resolved))
}
pub fn guest_path(&self) -> &Path {
&self.guest_path
}
pub fn mke2fs(&self) -> &GuestTool {
&self.mke2fs
}
pub fn resize2fs(&self) -> &GuestTool {
&self.resize2fs
}
pub fn id(&self) -> &str {
&self.combined_id
}
fn resolve() -> BoxliteResult<Self> {
let guest = GuestBinary::get()?;
let mke2fs = find_binary(HOST_GUEST_MKE2FS)?;
let resize2fs = find_binary(HOST_GUEST_RESIZE2FS)?;
Self::resolve_at(guest, mke2fs, resize2fs)
}
pub(crate) fn resolve_at(
guest: &GuestBinary,
mke2fs: PathBuf,
resize2fs: PathBuf,
) -> BoxliteResult<Self> {
let mke2fs_id = content_id(&mke2fs)?;
let resize2fs_id = content_id(&resize2fs)?;
let mut hasher = Sha256::new();
hasher.update(guest.id().as_bytes());
hasher.update(mke2fs_id.as_bytes());
hasher.update(resize2fs_id.as_bytes());
let combined_id = hex::encode(hasher.finalize())[..ID_LEN].to_string();
tracing::info!(
guest_id = %guest.id(),
mke2fs_id = %mke2fs_id,
resize2fs_id = %resize2fs_id,
combined_id = %combined_id,
"Resolved guest artifacts"
);
Ok(Self {
guest_path: guest.path().to_path_buf(),
mke2fs: GuestTool { path: mke2fs },
resize2fs: GuestTool { path: resize2fs },
combined_id,
})
}
}
fn content_id(path: &Path) -> BoxliteResult<String> {
let bytes = std::fs::read(path).map_err(|e| {
BoxliteError::Storage(format!("Cannot read guest tool {}: {}", path.display(), e))
})?;
validate_guest_bytes(&bytes, path)?;
Ok(hex::encode(Sha256::digest(&bytes))[..ID_LEN].to_string())
}
#[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 resolve(dir: &tempfile::TempDir, guest_filler: u8, tool_filler: u8) -> GuestArtifacts {
let guest_path = dir.path().join("boxlite-guest");
std::fs::write(&guest_path, fake_guest(guest_filler)).unwrap();
let guest = GuestBinary::resolve_at(guest_path).unwrap();
let mke2fs = dir.path().join("guest-mke2fs");
let resize2fs = dir.path().join("guest-resize2fs");
std::fs::write(&mke2fs, fake_guest(tool_filler)).unwrap();
std::fs::write(&resize2fs, fake_guest(tool_filler)).unwrap();
GuestArtifacts::resolve_at(&guest, mke2fs, resize2fs).unwrap()
}
#[test]
fn id_tracks_tool_bytes_on_disk() {
let dir = tempfile::tempdir().unwrap();
let before = resolve(&dir, 0xAA, 0x11);
let first_id = before.id().to_string();
let after = resolve(&dir, 0xAA, 0x22);
assert_ne!(
first_id,
after.id(),
"a rebuilt guest tool must not keep the previous identity"
);
}
#[test]
fn id_is_stable_for_unchanged_bytes() {
let dir = tempfile::tempdir().unwrap();
let first = resolve(&dir, 0xAA, 0x11);
let second = resolve(&dir, 0xAA, 0x11);
assert_eq!(
first.id(),
second.id(),
"identical bytes must reuse the cached minimal rootfs"
);
assert_eq!(first.id().len(), ID_LEN);
}
#[test]
fn a_missing_tool_is_reported_by_name() {
let dir = tempfile::tempdir().unwrap();
let guest_path = dir.path().join("boxlite-guest");
std::fs::write(&guest_path, fake_guest(0xAA)).unwrap();
let guest = GuestBinary::resolve_at(guest_path).unwrap();
let error = GuestArtifacts::resolve_at(
&guest,
dir.path().join("guest-mke2fs"),
dir.path().join("guest-resize2fs"),
)
.err()
.expect("a missing tool must not resolve");
assert!(
error.to_string().contains("Cannot read") && error.to_string().contains("guest-mke2fs"),
"error should name the unreadable tool: {error}"
);
}
#[test]
fn a_non_elf_tool_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let guest_path = dir.path().join("boxlite-guest");
std::fs::write(&guest_path, fake_guest(0xAA)).unwrap();
let guest = GuestBinary::resolve_at(guest_path).unwrap();
let mke2fs = dir.path().join("guest-mke2fs");
let resize2fs = dir.path().join("guest-resize2fs");
std::fs::write(&mke2fs, vec![0u8; 128]).unwrap();
std::fs::write(&resize2fs, vec![0u8; 128]).unwrap();
let error = GuestArtifacts::resolve_at(&guest, mke2fs, resize2fs)
.err()
.expect("a non-ELF tool must not resolve");
assert!(
error.to_string().contains("ELF"),
"error should name the problem: {error}"
);
}
}