use std::fs;
use std::path::{Path, PathBuf};
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use crate::disk::{
BaseDisk, BaseDiskKind, BaseDiskManager, Disk, DiskFormat, create_ext4_from_dir,
inject_file_into_ext4,
};
use crate::images::{ImageDiskManager, ImageObject};
#[cfg(test)]
use crate::runtime::id::BaseDiskID;
use crate::runtime::id::BaseDiskIDMint;
use crate::vmm::guest_artifacts::GuestArtifacts;
use crate::vmm::guest_binary::GuestBinary;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct GuestRootfs {
pub path: PathBuf,
pub strategy: Strategy,
pub kernel: Option<PathBuf>,
pub initrd: Option<PathBuf>,
#[serde(default)]
pub env: Vec<(String, String)>,
}
#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum Strategy {
#[default]
Direct,
Extracted {
layers: usize,
},
OverlayMount {
lower: Vec<PathBuf>,
upper: PathBuf,
work: PathBuf,
},
Disk {
disk_path: PathBuf,
device_path: Option<String>,
},
}
impl GuestRootfs {
pub fn new(
path: PathBuf,
strategy: Strategy,
kernel: Option<PathBuf>,
initrd: Option<PathBuf>,
env: Vec<(String, String)>,
) -> BoxliteResult<Self> {
match &strategy {
Strategy::Disk { disk_path, .. } => {
tracing::debug!(
"Skipping guest binary injection for disk-based rootfs: {}",
disk_path.display()
);
}
_ => {
crate::util::inject_guest_binary(&path)?;
}
}
Ok(Self {
path,
strategy,
kernel,
initrd,
env,
})
}
pub fn cleanup(&self) -> BoxliteResult<()> {
match &self.strategy {
Strategy::Direct => {
tracing::debug!(
"Skipping cleanup for direct rootfs: {}",
self.path.display()
);
Ok(())
}
Strategy::Extracted { layers } => {
tracing::info!(
"Cleaning up extracted rootfs ({} layers): {}",
layers,
self.path.display()
);
if let Some(parent) = self.path.parent() {
Self::remove_directory(parent)
} else {
Self::remove_directory(&self.path)
}
}
Strategy::OverlayMount { .. } => {
tracing::info!("Cleaning up overlay mount: {}", self.path.display());
#[cfg(target_os = "linux")]
{
Self::unmount_overlay(&self.path)?;
}
if let Some(parent) = self.path.parent() {
Self::remove_directory(parent)
} else {
Ok(())
}
}
Strategy::Disk { disk_path, .. } => {
tracing::debug!(
"Skipping cleanup for disk-based rootfs: {} (managed by cache)",
disk_path.display()
);
Ok(())
}
}
}
#[cfg(target_os = "linux")]
fn unmount_overlay(merged_dir: &Path) -> BoxliteResult<()> {
if !merged_dir.exists() {
return Ok(());
}
match std::process::Command::new("umount")
.arg(merged_dir)
.status()
{
Ok(status) if status.success() => {
tracing::debug!("Unmounted overlay: {}", merged_dir.display());
Ok(())
}
Ok(status) => {
tracing::warn!(
"Failed to unmount overlay {}: exit status {}",
merged_dir.display(),
status
);
Err(BoxliteError::Storage(format!(
"umount failed with status {}",
status
)))
}
Err(e) => {
tracing::warn!(
"Failed to execute umount for {}: {}",
merged_dir.display(),
e
);
Err(BoxliteError::Storage(format!(
"umount execution failed: {}",
e
)))
}
}
}
fn remove_directory(path: &Path) -> BoxliteResult<()> {
if let Err(e) = std::fs::remove_dir_all(path) {
tracing::warn!(
"Failed to cleanup rootfs directory {}: {}",
path.display(),
e
);
Err(BoxliteError::Storage(format!("cleanup failed: {}", e)))
} else {
tracing::info!("Cleaned up rootfs directory: {}", path.display());
Ok(())
}
}
}
pub struct GuestRootfsManager {
base_disk_mgr: BaseDiskManager,
temp_dir: PathBuf,
}
const GLOBAL_SOURCE: &str = "__global__";
impl GuestRootfsManager {
pub fn new(base_disk_mgr: BaseDiskManager, temp_dir: PathBuf) -> Self {
Self {
base_disk_mgr,
temp_dir,
}
}
pub async fn get_or_create(
&self,
image: &ImageObject,
image_disk_mgr: &ImageDiskManager,
env: Vec<(String, String)>,
) -> BoxliteResult<GuestRootfs> {
let total_start = std::time::Instant::now();
let stage1_start = std::time::Instant::now();
let image_disk = image_disk_mgr.get_or_create(image).await?;
tracing::info!(
elapsed_ms = stage1_start.elapsed().as_millis() as u64,
"get_or_create: stage1 image_disk done"
);
let digest = image.compute_image_digest();
let guest = GuestBinary::get()?;
let version_key = Self::version_key(&digest, guest.id());
if let Some(disk) = self.find(&version_key) {
tracing::info!(
version_key = %version_key,
commit = boxlite_shared::GIT_COMMIT.unwrap_or("unknown"),
total_ms = total_start.elapsed().as_millis() as u64,
"get_or_create: CACHE HIT"
);
return Self::disk_to_guest_rootfs(disk, env);
}
tracing::info!(
version_key = %version_key,
"get_or_create: CACHE MISS — building guest rootfs"
);
let disk = self.build_and_install(&image_disk, &version_key).await?;
tracing::info!(
total_ms = total_start.elapsed().as_millis() as u64,
cache_hit = false,
"get_or_create: completed"
);
Self::disk_to_guest_rootfs(disk, env)
}
pub async fn get_or_create_minimal(
&self,
artifacts: &GuestArtifacts,
) -> BoxliteResult<GuestRootfs> {
let key = Self::minimal_version_key(artifacts);
if let Some(disk) = self.find(&key) {
tracing::debug!(version_key = %key, "get_or_create_minimal: cache hit");
return Self::disk_to_guest_rootfs(disk, Vec::new());
}
tracing::info!(version_key = %key, "get_or_create_minimal: cache miss — building");
let disk = self.build_minimal_and_install(artifacts, &key).await?;
Self::disk_to_guest_rootfs(disk, Vec::new())
}
async fn build_minimal_and_install(
&self,
artifacts: &GuestArtifacts,
key: &str,
) -> BoxliteResult<Disk> {
let temp = tempfile::tempdir_in(&self.temp_dir).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to create minimal rootfs temp dir in {}: {}",
self.temp_dir.display(),
e
))
})?;
let tree_root = temp.path().join("rootfs");
stage_tree(&tree_root, artifacts)?;
let staged_path = temp.path().join("minimal-rootfs.ext4");
let tree_for_build = tree_root.clone();
let staged_for_build = staged_path.clone();
let disk = tokio::task::spawn_blocking(move || {
create_ext4_from_dir(&tree_for_build, &staged_for_build, 0)
})
.await
.map_err(|e| BoxliteError::Storage(format!("minimal rootfs build task failed: {e}")))??;
self.install(key, disk)
}
fn disk_to_guest_rootfs(disk: Disk, env: Vec<(String, String)>) -> BoxliteResult<GuestRootfs> {
let disk_path = disk.path().to_path_buf();
let _ = disk.leak();
GuestRootfs::new(
disk_path.clone(),
Strategy::Disk {
disk_path,
device_path: None,
},
None,
None,
env,
)
}
fn find(&self, version_key: &str) -> Option<Disk> {
let record = self
.base_disk_mgr
.store()
.find_by_name(GLOBAL_SOURCE, version_key)
.ok()
.flatten()?;
let path = PathBuf::from(record.base_path());
if path.exists() {
Some(Disk::new(path, DiskFormat::Ext4, true))
} else {
tracing::warn!(
version_key = %version_key,
base_path = %record.base_path(),
"DB record exists but file missing, removing stale record"
);
let _ = self.base_disk_mgr.store().delete(record.id());
None
}
}
async fn build_and_install(&self, image_disk: &Disk, version_key: &str) -> BoxliteResult<Disk> {
let build_start = std::time::Instant::now();
let temp = tempfile::tempdir_in(&self.temp_dir).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to create temp directory in {}: {}",
self.temp_dir.display(),
e
))
})?;
let staged_path = temp.path().join("guest-rootfs.ext4");
let copy_start = std::time::Instant::now();
let copy_bytes = fs::copy(image_disk.path(), &staged_path).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to copy image disk {} to staged path {}: {}",
image_disk.path().display(),
staged_path.display(),
e
))
})?;
tracing::info!(
elapsed_ms = copy_start.elapsed().as_millis() as u64,
size_mb = copy_bytes / (1024 * 1024),
"build_and_install: copy image disk done"
);
let inject_start = std::time::Instant::now();
let guest = GuestBinary::get()?;
inject_file_into_ext4(&staged_path, guest.path(), "boxlite/bin/boxlite-guest")?;
tracing::info!(
elapsed_ms = inject_start.elapsed().as_millis() as u64,
"build_and_install: inject guest binary done"
);
let staged_disk = Disk::new(staged_path, DiskFormat::Ext4, false);
let result = self.install(version_key, staged_disk);
tracing::info!(
version_key = %version_key,
commit = boxlite_shared::GIT_COMMIT.unwrap_or("unknown"),
guest_id = %guest.id(),
total_ms = build_start.elapsed().as_millis() as u64,
"build_and_install: completed"
);
result
}
fn install(&self, version_key: &str, staged_disk: Disk) -> BoxliteResult<Disk> {
if let Some(disk) = self.find(version_key) {
tracing::debug!(version_key = %version_key, "Guest rootfs already installed (race)");
return Ok(disk);
}
let bases_dir = self.base_disk_mgr.bases_dir();
fs::create_dir_all(bases_dir).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to create bases directory {}: {}",
bases_dir.display(),
e
))
})?;
let layer_id = BaseDiskIDMint::mint();
let target = bases_dir.join(format!("{}.ext4", layer_id));
let source = staged_disk.path().to_path_buf();
fs::rename(&source, &target).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to install guest rootfs from {} to {}: {}",
source.display(),
target.display(),
e
))
})?;
let _ = staged_disk.leak();
let size_bytes = fs::metadata(&target).map(|m| m.len()).unwrap_or(0);
let disk = BaseDisk {
id: layer_id.clone(),
source_box_id: GLOBAL_SOURCE.to_string(),
name: Some(version_key.to_string()),
kind: BaseDiskKind::Rootfs,
disk_info: crate::disk::DiskInfo {
base_path: target.to_string_lossy().to_string(),
container_disk_bytes: 0,
size_bytes,
},
created_at: chrono::Utc::now().timestamp(),
};
if let Err(e) = self.base_disk_mgr.store().insert(&disk) {
tracing::warn!(
version_key = %version_key,
error = %e,
"DB insert failed (possible race), checking for existing entry"
);
let _ = fs::remove_file(&target);
if let Some(disk) = self.find(version_key) {
return Ok(disk);
}
return Err(e);
}
tracing::info!(
layer_id = %layer_id,
version_key = %version_key,
path = %target.display(),
"Installed guest rootfs to cache"
);
Ok(Disk::new(target, DiskFormat::Ext4, true))
}
pub fn gc(&self, boxes_dir: &Path) -> BoxliteResult<usize> {
let gc_start = std::time::Instant::now();
let current_minimal_key = GuestArtifacts::get()?.id().to_string();
let result = self.gc_inner(boxes_dir, Some(¤t_minimal_key));
tracing::info!(
elapsed_ms = gc_start.elapsed().as_millis() as u64,
"GC completed"
);
result
}
fn gc_inner(
&self,
boxes_dir: &Path,
current_minimal_key: Option<&str>,
) -> BoxliteResult<usize> {
let records = self
.base_disk_mgr
.store()
.list_by_box(GLOBAL_SOURCE, Some(BaseDiskKind::Rootfs))?;
if records.is_empty() {
return Ok(0);
}
let referenced = self.base_disk_mgr.referenced_backing_paths(boxes_dir);
tracing::info!(
referenced_count = referenced.len(),
total_records = records.len(),
"gc_inner: scanned boxes for references"
);
let mut removed = 0;
let mut preserved_current = 0;
let mut preserved_referenced = 0;
for record in &records {
let base_path = PathBuf::from(record.base_path());
let version_key = record.name().unwrap_or("");
if referenced.contains(&base_path) {
preserved_referenced += 1;
continue;
}
if current_minimal_key == Some(version_key) {
preserved_current += 1;
tracing::debug!(
version_key = %version_key,
"GC: keeping current minimal rootfs"
);
continue;
}
tracing::info!(
id = %record.id(),
version_key = %version_key,
path = %record.base_path(),
"GC: removing stale guest rootfs"
);
if let Err(e) = fs::remove_file(&base_path)
&& base_path.exists()
{
tracing::warn!("GC: failed to remove {}: {}", base_path.display(), e);
}
if let Err(e) = self.base_disk_mgr.store().delete(record.id()) {
tracing::warn!("GC: failed to delete DB record {}: {}", record.id(), e);
} else {
removed += 1;
}
}
tracing::info!(
total_entries = records.len(),
preserved_current,
preserved_referenced,
removed,
"gc_inner: summary"
);
Ok(removed)
}
fn version_key(digest: &str, guest_hash: &str) -> String {
let d = digest.strip_prefix("sha256:").unwrap_or(digest);
let d = &d[..12.min(d.len())];
let g = &guest_hash[..12.min(guest_hash.len())];
format!("{}-{}", d, g)
}
fn minimal_version_key(artifacts: &GuestArtifacts) -> String {
artifacts.id().to_string()
}
}
fn stage_tree(rootfs: &Path, artifacts: &GuestArtifacts) -> BoxliteResult<()> {
let bin = rootfs.join("boxlite").join("bin");
std::fs::create_dir_all(&bin).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to create minimal rootfs tree {}: {}",
bin.display(),
e
))
})?;
copy_artifact(artifacts.guest_path(), &bin.join("boxlite-guest"))?;
copy_artifact(artifacts.mke2fs().path(), &bin.join("mke2fs"))?;
copy_artifact(artifacts.resize2fs().path(), &bin.join("resize2fs"))?;
copy_artifact(artifacts.mke2fs().path(), &bin.join("mkfs.ext4"))?;
for dir in ["tmp", "var/tmp", "run", "dev", "proc", "sys"] {
std::fs::create_dir_all(rootfs.join(dir)).map_err(|e| {
BoxliteError::Storage(format!("Failed to create mount point {dir}: {e}"))
})?;
}
Ok(())
}
fn copy_artifact(src: &Path, dst: &Path) -> BoxliteResult<()> {
std::fs::copy(src, dst).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to copy {} -> {}: {}",
src.display(),
dst.display(),
e
))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dst, std::fs::Permissions::from_mode(0o755)).map_err(|e| {
BoxliteError::Storage(format!("Failed to chmod 0755 {}: {}", dst.display(), e))
})?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::Database;
use crate::db::base_disk::BaseDiskStore;
fn id(s: &str) -> BaseDiskID {
BaseDiskID::parse(s).expect("test ID must be valid Base62 length-8")
}
fn test_store() -> BaseDiskStore {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.keep().join("test.db");
let db = Database::open(&db_path).unwrap();
BaseDiskStore::new(db)
}
fn make_mgr(bases_dir: PathBuf, temp_dir: PathBuf) -> GuestRootfsManager {
let base_disk_mgr = BaseDiskManager::new(bases_dir, test_store());
GuestRootfsManager::new(base_disk_mgr, temp_dir)
}
fn insert_rootfs_record(store: &BaseDiskStore, rootfs_id: &str, version_key: &str, path: &str) {
store
.insert(&BaseDisk {
id: id(rootfs_id),
source_box_id: GLOBAL_SOURCE.to_string(),
name: Some(version_key.to_string()),
kind: BaseDiskKind::Rootfs,
disk_info: crate::disk::DiskInfo {
base_path: path.to_string(),
container_disk_bytes: 0,
size_bytes: 100,
},
created_at: chrono::Utc::now().timestamp(),
})
.unwrap();
}
#[test]
fn test_version_key_strips_sha256_prefix() {
let key = GuestRootfsManager::version_key(
"sha256:abcdef123456789012345678",
"fedcba987654321012345678",
);
assert_eq!(key, "abcdef123456-fedcba987654");
}
#[test]
fn test_version_key_no_prefix() {
let key = GuestRootfsManager::version_key("abcdef123456789012", "111222333444555666");
assert_eq!(key, "abcdef123456-111222333444");
}
#[test]
fn test_version_key_short_inputs() {
let key = GuestRootfsManager::version_key("abc", "def");
assert_eq!(key, "abc-def");
}
#[test]
fn version_key_follows_the_guest_binary_on_disk() {
let dir = tempfile::TempDir::new().unwrap();
let digest = "sha256:18264223e3ecaaaabbbbccccdddd";
let write_guest = |filler: u8| {
let path = dir.path().join("boxlite-guest");
let mut elf = vec![0u8; 128];
elf[..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
elf[4] = 2;
let machine: u16 = if std::env::consts::ARCH == "x86_64" {
0x3E
} else {
0xB7
};
elf[18..20].copy_from_slice(&machine.to_le_bytes());
elf[64..].fill(filler);
std::fs::write(&path, elf).unwrap();
path
};
let before = GuestBinary::resolve_at(write_guest(0xAA)).unwrap();
let after = GuestBinary::resolve_at(write_guest(0xBB)).unwrap();
let key_before = GuestRootfsManager::version_key(digest, before.id());
let key_after = GuestRootfsManager::version_key(digest, after.id());
assert_ne!(
key_before, key_after,
"a rebuilt guest must not reuse the previous rootfs cache entry"
);
assert_eq!(
key_before[..12],
key_after[..12],
"image half must be stable"
);
assert!(key_after.ends_with(after.id()), "GC matches on this suffix");
}
#[test]
fn test_find_returns_none_for_missing() {
let dir = tempfile::TempDir::new().unwrap();
let mgr = make_mgr(dir.path().to_path_buf(), dir.path().to_path_buf());
assert!(mgr.find("nonexistent-key").is_none());
}
#[test]
fn test_find_returns_disk_for_existing_db_record() {
let dir = tempfile::TempDir::new().unwrap();
let bases_dir = dir.path().to_path_buf();
let store = test_store();
let cached = bases_dir.join("aB3xQ9mP.ext4");
std::fs::write(&cached, "fake disk").unwrap();
insert_rootfs_record(&store, "aB3xQ9mP", "test-version", cached.to_str().unwrap());
let base_disk_mgr = BaseDiskManager::new(bases_dir, store);
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
let disk = mgr.find("test-version");
assert!(disk.is_some());
let disk = disk.unwrap();
assert_eq!(disk.path(), cached);
assert_eq!(disk.format(), DiskFormat::Ext4);
let _ = disk.leak();
}
#[test]
fn test_find_returns_none_when_file_missing_despite_db_record() {
let dir = tempfile::TempDir::new().unwrap();
let store = test_store();
insert_rootfs_record(
&store,
"aB3xQ9mP",
"ghost-key",
dir.path().join("ghost.ext4").to_str().unwrap(),
);
let base_disk_mgr = BaseDiskManager::new(dir.path().to_path_buf(), store.clone());
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
assert!(mgr.find("ghost-key").is_none());
assert!(
store
.find_by_name(GLOBAL_SOURCE, "ghost-key")
.unwrap()
.is_none()
);
}
#[test]
fn test_install_creates_bases_dir_and_moves_file() {
let dir = tempfile::TempDir::new().unwrap();
let bases_dir = dir.path().join("bases");
let store = test_store();
let base_disk_mgr = BaseDiskManager::new(bases_dir.clone(), store.clone());
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
let staged_path = dir.path().join("staged.ext4");
std::fs::write(&staged_path, "staged disk content").unwrap();
let staged_disk = Disk::new(staged_path, DiskFormat::Ext4, false);
let result = mgr.install("ver-key", staged_disk).unwrap();
assert!(result.path().starts_with(&bases_dir));
assert_eq!(result.path().extension().unwrap(), "ext4");
assert!(result.path().exists());
let stem = result.path().file_stem().unwrap().to_string_lossy();
assert!(
BaseDiskID::parse(&stem).is_some(),
"rootfs filename should be valid BaseDiskID"
);
let record = store.find_by_name(GLOBAL_SOURCE, "ver-key").unwrap();
assert!(record.is_some());
let record = record.unwrap();
assert_eq!(record.kind(), BaseDiskKind::Rootfs);
assert_eq!(record.base_path(), result.path().to_string_lossy());
let _ = result.leak();
}
#[test]
fn test_install_race_safe_returns_existing() {
let dir = tempfile::TempDir::new().unwrap();
let bases_dir = dir.path().join("bases");
std::fs::create_dir_all(&bases_dir).unwrap();
let store = test_store();
let existing = bases_dir.join("first123.ext4");
std::fs::write(&existing, "first install").unwrap();
insert_rootfs_record(&store, "first123", "raced-key", existing.to_str().unwrap());
let base_disk_mgr = BaseDiskManager::new(bases_dir, store);
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
let staged_path = dir.path().join("staged.ext4");
std::fs::write(&staged_path, "second install").unwrap();
let staged_disk = Disk::new(staged_path, DiskFormat::Ext4, false);
let result = mgr.install("raced-key", staged_disk).unwrap();
assert_eq!(result.path(), existing);
assert_eq!(
std::fs::read_to_string(result.path()).unwrap(),
"first install"
);
let _ = result.leak();
}
#[test]
fn test_gc_removes_stale_entries() {
let dir = tempfile::TempDir::new().unwrap();
let bases_dir = dir.path().join("bases");
let boxes_dir = dir.path().join("boxes");
std::fs::create_dir_all(&bases_dir).unwrap();
std::fs::create_dir_all(&boxes_dir).unwrap();
let store = test_store();
let file1 = bases_dir.join("aaa11111.ext4");
let file2 = bases_dir.join("bbb22222.ext4");
std::fs::write(&file1, "old1").unwrap();
std::fs::write(&file2, "old2").unwrap();
insert_rootfs_record(
&store,
"aaa11111",
"img123-oldguest1",
file1.to_str().unwrap(),
);
insert_rootfs_record(
&store,
"bbb22222",
"img456-oldguest2",
file2.to_str().unwrap(),
);
let base_disk_mgr = BaseDiskManager::new(bases_dir, store);
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
let removed = mgr.gc_inner(&boxes_dir, None).unwrap();
assert_eq!(removed, 2);
assert!(!file1.exists());
assert!(!file2.exists());
}
#[test]
fn test_gc_preserves_current_minimal_entry() {
let dir = tempfile::TempDir::new().unwrap();
let bases_dir = dir.path().join("bases");
let boxes_dir = dir.path().join("boxes");
std::fs::create_dir_all(&bases_dir).unwrap();
std::fs::create_dir_all(&boxes_dir).unwrap();
let store = test_store();
let current_file = bases_dir.join("ccc33333.ext4");
std::fs::write(¤t_file, "current minimal").unwrap();
insert_rootfs_record(
&store,
"ccc33333",
"minimal-current",
current_file.to_str().unwrap(),
);
let stale_file = bases_dir.join("ddd44444.ext4");
std::fs::write(&stale_file, "stale").unwrap();
insert_rootfs_record(
&store,
"ddd44444",
"minimal-old",
stale_file.to_str().unwrap(),
);
let base_disk_mgr = BaseDiskManager::new(bases_dir, store);
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
let removed = mgr.gc_inner(&boxes_dir, Some("minimal-current")).unwrap();
assert_eq!(removed, 1);
assert!(
current_file.exists(),
"Current minimal entry should be kept"
);
assert!(!stale_file.exists(), "Stale entry should be removed");
}
#[test]
fn test_gc_preserves_referenced_entries() {
let dir = tempfile::TempDir::new().unwrap();
let bases_dir = dir.path().join("bases");
let boxes_dir = dir.path().join("boxes");
std::fs::create_dir_all(&bases_dir).unwrap();
let store = test_store();
let referenced_file = bases_dir.join("eee55555.ext4");
std::fs::write(&referenced_file, "keep me").unwrap();
insert_rootfs_record(
&store,
"eee55555",
"img123-oldguest",
referenced_file.to_str().unwrap(),
);
let unreferenced_file = bases_dir.join("fff66666.ext4");
std::fs::write(&unreferenced_file, "delete me").unwrap();
insert_rootfs_record(
&store,
"fff66666",
"img456-oldguest",
unreferenced_file.to_str().unwrap(),
);
let box_disks = boxes_dir.join("box-1").join("disks");
std::fs::create_dir_all(&box_disks).unwrap();
let qcow2_path = box_disks.join("guest-rootfs.qcow2");
let backing_str = referenced_file.to_str().unwrap();
let backing_bytes = backing_str.as_bytes();
let mut buf = vec![0u8; 1024];
buf[0..4].copy_from_slice(&0x514649fbu32.to_be_bytes()); buf[4..8].copy_from_slice(&3u32.to_be_bytes()); buf[8..16].copy_from_slice(&512u64.to_be_bytes()); buf[16..20].copy_from_slice(&(backing_bytes.len() as u32).to_be_bytes());
buf[512..512 + backing_bytes.len()].copy_from_slice(backing_bytes);
std::fs::write(&qcow2_path, &buf).unwrap();
let base_disk_mgr = BaseDiskManager::new(bases_dir, store);
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
let removed = mgr.gc_inner(&boxes_dir, None).unwrap();
assert_eq!(removed, 1);
assert!(referenced_file.exists(), "Referenced entry should be kept");
assert!(
!unreferenced_file.exists(),
"Unreferenced stale entry should be removed"
);
}
#[test]
fn test_gc_no_records() {
let dir = tempfile::TempDir::new().unwrap();
let mgr = make_mgr(dir.path().join("bases"), dir.path().to_path_buf());
let removed = mgr.gc_inner(dir.path(), None).unwrap();
assert_eq!(removed, 0);
}
#[test]
fn test_gc_no_boxes_dir() {
let dir = tempfile::TempDir::new().unwrap();
let bases_dir = dir.path().join("bases");
std::fs::create_dir_all(&bases_dir).unwrap();
let store = test_store();
let stale = bases_dir.join("ggg77777.ext4");
std::fs::write(&stale, "orphan").unwrap();
insert_rootfs_record(&store, "ggg77777", "img-oldguest", stale.to_str().unwrap());
let base_disk_mgr = BaseDiskManager::new(bases_dir, store);
let mgr = GuestRootfsManager::new(base_disk_mgr, dir.path().to_path_buf());
let removed = mgr
.gc_inner(&dir.path().join("nonexistent-boxes"), None)
.unwrap();
assert_eq!(removed, 1);
}
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 fake_artifacts(dir: &tempfile::TempDir) -> GuestArtifacts {
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, fake_guest(0x11)).unwrap();
std::fs::write(&resize2fs, fake_guest(0x22)).unwrap();
GuestArtifacts::resolve_at(&guest, mke2fs, resize2fs).unwrap()
}
#[test]
fn stage_tree_copies_artifacts_and_mkfs_ext4_alias() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let artifacts = fake_artifacts(&dir);
let tree = dir.path().join("rootfs");
stage_tree(&tree, &artifacts).unwrap();
let bin = tree.join("boxlite").join("bin");
for name in ["boxlite-guest", "mke2fs", "resize2fs", "mkfs.ext4"] {
let path = bin.join(name);
assert!(path.is_file(), "{name} must be staged");
assert_eq!(
std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
0o755,
"{name} must be mode 0755"
);
}
assert_eq!(
std::fs::read(bin.join("mkfs.ext4")).unwrap(),
std::fs::read(bin.join("mke2fs")).unwrap(),
"mkfs.ext4 must be a byte-copy of mke2fs"
);
for dir in ["tmp", "var/tmp", "run", "dev", "proc", "sys"] {
assert!(tree.join(dir).is_dir(), "{dir} mount point must be staged");
}
}
#[tokio::test]
async fn get_or_create_minimal_builds_ext4_with_artifacts() {
use crate::util::find_binary;
if find_binary("mke2fs").is_err() || find_binary("debugfs").is_err() {
eprintln!("skipping: mke2fs/debugfs not found (run `make runtime:debug`)");
return;
}
let dir = tempfile::tempdir().unwrap();
let artifacts = fake_artifacts(&dir);
let temp_dir = dir.path().join("tmp");
std::fs::create_dir_all(&temp_dir).unwrap();
let mgr = make_mgr(dir.path().join("bases"), temp_dir);
let rootfs = mgr.get_or_create_minimal(&artifacts).await.unwrap();
let Strategy::Disk { disk_path, .. } = rootfs.strategy else {
panic!("minimal rootfs must be disk-based");
};
assert!(disk_path.is_file());
let debugfs = find_binary("debugfs").unwrap();
let ls = std::process::Command::new(&debugfs)
.args(["-R", "ls /boxlite/bin"])
.arg(&disk_path)
.output()
.expect("run debugfs ls");
assert!(
ls.status.success(),
"debugfs ls failed: {}",
String::from_utf8_lossy(&ls.stderr)
);
let listing = String::from_utf8_lossy(&ls.stdout);
for name in ["boxlite-guest", "mke2fs", "resize2fs", "mkfs.ext4"] {
assert!(
listing.contains(name),
"image /boxlite/bin must contain {name}:\n{listing}"
);
}
let rootfs2 = mgr.get_or_create_minimal(&artifacts).await.unwrap();
let Strategy::Disk {
disk_path: disk_path2,
..
} = rootfs2.strategy
else {
panic!("minimal rootfs must be disk-based");
};
assert_eq!(disk_path, disk_path2);
}
}