use std::path::Path;
use anyhow::{Context, Result, bail};
use tokio::process::Command;
use uuid::Uuid;
const OCI2ROOTFS_BIN: &str = "/arcbox/bin/oci2rootfs";
const VM_AGENT_BIN: &str = "/arcbox/bin/vm-agent";
const ROOTFS_CACHE_DIR: &str = "/arcbox/bin";
pub fn has_ext4_magic(path: &Path) -> bool {
use std::io::{Read, Seek, SeekFrom};
let Ok(mut file) = std::fs::File::open(path) else {
return false;
};
let mut magic = [0u8; 2];
file.seek(SeekFrom::Start(0x438)).is_ok()
&& file.read_exact(&mut magic).is_ok()
&& magic == [0x53, 0xEF]
}
pub async fn convert_layer_to_rootfs(layer_path: &str) -> Result<String> {
if !Path::new(layer_path).exists() {
bail!("layer path not found: {layer_path}");
}
let hash = path_hash(layer_path);
let ext4_path = format!("{ROOTFS_CACHE_DIR}/rootfs-{hash}.ext4");
if Path::new(&ext4_path).exists() && has_ext4_magic(Path::new(&ext4_path)) {
tracing::info!(path = %ext4_path, "using cached rootfs");
return Ok(ext4_path);
}
let req_id = Uuid::new_v4().to_string();
let ext4_tmp = format!("{ROOTFS_CACHE_DIR}/.rootfs-{req_id}.ext4.tmp");
tracing::info!(layer = %layer_path, ext4 = %ext4_path, "converting overlay2 layer to ext4");
run_oci2rootfs(layer_path, &ext4_tmp).await?;
tracing::info!("injecting vm-agent into rootfs");
inject_vm_agent(&ext4_tmp, &req_id).await?;
tokio::fs::rename(&ext4_tmp, &ext4_path)
.await
.context("failed to rename ext4 into cache")?;
tracing::info!(path = %ext4_path, "rootfs ready");
Ok(ext4_path)
}
async fn run_oci2rootfs(layer_path: &str, output: &str) -> Result<()> {
if !Path::new(OCI2ROOTFS_BIN).exists() {
bail!("oci2rootfs not found at {OCI2ROOTFS_BIN}");
}
let result = Command::new(OCI2ROOTFS_BIN)
.args([layer_path, "--output", output])
.output()
.await
.context("failed to spawn oci2rootfs")?;
if !result.status.success() {
let stderr = String::from_utf8_lossy(&result.stderr);
bail!("oci2rootfs failed: {stderr}");
}
Ok(())
}
async fn inject_vm_agent(ext4_path: &str, req_id: &str) -> Result<()> {
if !Path::new(VM_AGENT_BIN).exists() {
bail!("vm-agent not found at {VM_AGENT_BIN}");
}
let mount_dir = format!("/tmp/arcbox-inject-{req_id}");
tokio::fs::create_dir_all(&mount_dir).await?;
let status = Command::new("/bin/busybox")
.args(["mount", "-o", "loop", ext4_path, &mount_dir])
.status()
.await
.context("failed to mount ext4 for vm-agent injection")?;
if !status.success() {
let _ = tokio::fs::remove_dir(&mount_dir).await;
bail!("mount -o loop failed");
}
let sbin = format!("{mount_dir}/sbin");
tokio::fs::create_dir_all(&sbin)
.await
.context("failed to create /sbin in rootfs")?;
let dest = format!("{sbin}/vm-agent");
let copy_result = tokio::fs::copy(VM_AGENT_BIN, &dest).await;
if copy_result.is_ok() {
let _ = Command::new("/bin/busybox")
.args(["chmod", "755", &dest])
.status()
.await;
}
let _ = Command::new("/bin/busybox")
.args(["umount", &mount_dir])
.status()
.await;
let _ = tokio::fs::remove_dir(&mount_dir).await;
copy_result.context("failed to copy vm-agent into rootfs")?;
Ok(())
}
fn path_hash(path: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
path.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_ext4_magic_nonexistent() {
assert!(!has_ext4_magic(Path::new("/nonexistent")));
}
#[test]
fn test_path_hash_deterministic() {
let a = path_hash("/var/lib/docker/overlay2/abc123");
let b = path_hash("/var/lib/docker/overlay2/abc123");
assert_eq!(a, b);
assert_eq!(a.len(), 16);
}
#[test]
fn test_path_hash_different() {
let a = path_hash("/var/lib/docker/overlay2/abc123");
let b = path_hash("/var/lib/docker/overlay2/def456");
assert_ne!(a, b);
}
}