use crate::util;
use boxlite_shared::{BoxliteError, BoxliteResult};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
use super::constants::ext4::{
BLOCK_SIZE, DEFAULT_DIR_SIZE_BYTES, INODE_SIZE, JOURNAL_OVERHEAD_BYTES, MIN_DISK_SIZE_BYTES,
SIZE_MULTIPLIER_DEN, SIZE_MULTIPLIER_NUM,
};
use super::{Disk, DiskFormat};
fn get_mke2fs_path() -> PathBuf {
util::find_binary("mke2fs").expect("mke2fs binary not found")
}
fn get_debugfs_path() -> PathBuf {
util::find_binary("debugfs").expect("debugfs binary not found")
}
fn calculate_dir_size(dir: &Path) -> BoxliteResult<u64> {
let mut total_blocks = 0u64;
let mut entry_count = 0u64;
for entry in WalkDir::new(dir).follow_links(false) {
let entry = entry.map_err(|e| {
BoxliteError::Storage(format!("Failed to walk directory {}: {}", dir.display(), e))
})?;
entry_count += 1;
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
let file_blocks = metadata.len().div_ceil(BLOCK_SIZE);
total_blocks += file_blocks.max(1);
} else if metadata.is_dir() {
total_blocks += 1;
}
}
}
let content_size = total_blocks * BLOCK_SIZE;
let inode_size = entry_count * INODE_SIZE;
Ok(content_size + inode_size)
}
fn calculate_disk_size(source: &Path) -> u64 {
let dir_size = calculate_dir_size(source).unwrap_or(DEFAULT_DIR_SIZE_BYTES);
let size_with_overhead =
dir_size * SIZE_MULTIPLIER_NUM / SIZE_MULTIPLIER_DEN + JOURNAL_OVERHEAD_BYTES;
let final_size = size_with_overhead.max(MIN_DISK_SIZE_BYTES);
tracing::debug!(
"Calculated disk size: dir_size={}MB, with_overhead={}MB, final={}MB",
dir_size / (1024 * 1024),
size_with_overhead / (1024 * 1024),
final_size / (1024 * 1024)
);
final_size
}
pub fn create_ext4_from_dir(source: &Path, output_path: &Path) -> BoxliteResult<Disk> {
let size_bytes = calculate_disk_size(source);
let size_blocks = size_bytes / 4096;
let output_str = output_path.to_str().ok_or_else(|| {
BoxliteError::Storage(format!("Invalid output path: {}", output_path.display()))
})?;
let source_str = source.to_str().ok_or_else(|| {
BoxliteError::Storage(format!("Invalid source path: {}", source.display()))
})?;
let mke2fs = get_mke2fs_path();
let output = Command::new(&mke2fs)
.args([
"-t",
"ext4",
"-b",
"4096", "-d",
source_str,
"-m",
"0",
"-E",
"root_owner=0:0",
"-F", "-q", output_str,
&size_blocks.to_string(),
])
.output()
.map_err(|e| {
BoxliteError::Storage(format!(
"Failed to run mke2fs ({}): {}",
mke2fs.display(),
e
))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BoxliteError::Storage(format!(
"mke2fs failed with exit code {:?}: {}",
output.status.code(),
stderr
)));
}
fix_ownership_with_debugfs(output_path, source)?;
Ok(Disk::new(
output_path.to_path_buf(),
DiskFormat::Ext4,
false,
))
}
fn fix_ownership_with_debugfs(image_path: &Path, source_dir: &Path) -> BoxliteResult<()> {
let current_uid = unsafe { libc::getuid() };
let current_gid = unsafe { libc::getgid() };
if current_uid == 0 && current_gid == 0 {
tracing::debug!("Running as root, skipping debugfs ownership fix");
return Ok(());
}
let start = std::time::Instant::now();
let mut paths = Vec::new();
for entry in WalkDir::new(source_dir).follow_links(false) {
let entry =
entry.map_err(|e| BoxliteError::Storage(format!("Failed to walk directory: {}", e)))?;
let rel_path = entry
.path()
.strip_prefix(source_dir)
.unwrap_or(entry.path());
if rel_path.as_os_str().is_empty() {
continue;
}
let ext4_path = format!("/{}", rel_path.display());
paths.push(ext4_path);
}
if paths.is_empty() {
tracing::debug!("No files to fix ownership for");
return Ok(());
}
let mut commands = String::new();
for path in &paths {
commands.push_str(&format!("sif {} uid 0\n", path));
commands.push_str(&format!("sif {} gid 0\n", path));
}
let debugfs = get_debugfs_path();
let mut child = Command::new(&debugfs)
.args(["-w", "-f", "-"])
.arg(image_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| BoxliteError::Storage(format!("Failed to spawn debugfs: {}", e)))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(commands.as_bytes()).map_err(|e| {
BoxliteError::Storage(format!("Failed to write to debugfs stdin: {}", e))
})?;
}
let output = child
.wait_with_output()
.map_err(|e| BoxliteError::Storage(format!("Failed to wait for debugfs: {}", e)))?;
let duration = start.elapsed();
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::warn!(
"debugfs ownership fix had errors (took {:?}): {}",
duration,
stderr
);
} else {
tracing::info!(
"Fixed ownership of {} files to 0:0 in {:?}",
paths.len(),
duration
);
}
Ok(())
}
pub fn inject_file_into_ext4(
image_path: &Path,
host_file: &Path,
guest_path: &str,
) -> BoxliteResult<()> {
let host_file_str = host_file.to_str().ok_or_else(|| {
BoxliteError::Storage(format!("Invalid host file path: {}", host_file.display()))
})?;
let commands = build_inject_commands(host_file_str, guest_path);
let debugfs = get_debugfs_path();
let mut child = Command::new(&debugfs)
.args(["-w", "-f", "-"])
.arg(image_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| {
BoxliteError::Storage(format!("Failed to spawn debugfs for injection: {}", e))
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(commands.as_bytes()).map_err(|e| {
BoxliteError::Storage(format!("Failed to write to debugfs stdin: {}", e))
})?;
}
let output = child
.wait_with_output()
.map_err(|e| BoxliteError::Storage(format!("Failed to wait for debugfs: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BoxliteError::Storage(format!(
"debugfs injection failed for {} -> {}: {}",
host_file.display(),
guest_path,
stderr
)));
}
tracing::debug!(
"Injected {} into ext4 image at /{}",
host_file.display(),
guest_path
);
Ok(())
}
fn build_inject_commands(host_file_str: &str, guest_path: &str) -> String {
let mut commands = String::new();
let guest_path_obj = Path::new(guest_path);
let mut current = PathBuf::new();
if let Some(parent) = guest_path_obj.parent() {
for component in parent.components() {
current.push(component);
commands.push_str(&format!("mkdir /{}\n", current.display()));
}
}
let ext4_dest = format!("/{}", guest_path);
commands.push_str(&format!("write \"{}\" {}\n", host_file_str, ext4_dest));
commands.push_str(&format!("sif {} uid 0\n", ext4_dest));
commands.push_str(&format!("sif {} gid 0\n", ext4_dest));
commands.push_str(&format!("sif {} mode 0100555\n", ext4_dest));
let mut current = PathBuf::new();
if let Some(parent) = guest_path_obj.parent() {
for component in parent.components() {
current.push(component);
let dir_path = format!("/{}", current.display());
commands.push_str(&format!("sif {} uid 0\n", dir_path));
commands.push_str(&format!("sif {} gid 0\n", dir_path));
}
}
commands
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_inject_commands_nested_path() {
let cmds = build_inject_commands("/host/boxlite-guest", "boxlite/bin/boxlite-guest");
assert!(cmds.contains("mkdir /boxlite\n"));
assert!(cmds.contains("mkdir /boxlite/bin\n"));
assert!(cmds.contains("write \"/host/boxlite-guest\" /boxlite/bin/boxlite-guest\n"));
assert!(cmds.contains("sif /boxlite/bin/boxlite-guest uid 0\n"));
assert!(cmds.contains("sif /boxlite/bin/boxlite-guest gid 0\n"));
assert!(cmds.contains("sif /boxlite/bin/boxlite-guest mode 0100555\n"));
assert!(cmds.contains("sif /boxlite uid 0\n"));
assert!(cmds.contains("sif /boxlite gid 0\n"));
assert!(cmds.contains("sif /boxlite/bin uid 0\n"));
assert!(cmds.contains("sif /boxlite/bin gid 0\n"));
}
#[test]
fn test_build_inject_commands_single_dir() {
let cmds = build_inject_commands("/host/file", "dir/file");
assert!(cmds.contains("mkdir /dir\n"));
assert!(cmds.contains("write \"/host/file\" /dir/file\n"));
assert!(cmds.contains("sif /dir uid 0\n"));
assert!(cmds.contains("sif /dir gid 0\n"));
}
#[test]
fn test_build_inject_commands_root_level_file() {
let cmds = build_inject_commands("/host/file", "file");
assert!(!cmds.contains("mkdir"));
assert!(cmds.contains("write \"/host/file\" /file\n"));
assert!(cmds.contains("sif /file uid 0\n"));
assert!(cmds.contains("sif /file gid 0\n"));
assert!(cmds.contains("sif /file mode 0100555\n"));
}
#[test]
fn test_build_inject_commands_deeply_nested() {
let cmds = build_inject_commands("/src/bin", "a/b/c/d/bin");
assert!(cmds.contains("mkdir /a\n"));
assert!(cmds.contains("mkdir /a/b\n"));
assert!(cmds.contains("mkdir /a/b/c\n"));
assert!(cmds.contains("mkdir /a/b/c/d\n"));
assert!(cmds.contains("write \"/src/bin\" /a/b/c/d/bin\n"));
}
#[test]
fn test_build_inject_commands_path_with_spaces() {
let cmds = build_inject_commands(
"/Users/user/Library/Application Support/boxlite/runtimes/v0.6.0/boxlite-guest",
"boxlite/bin/boxlite-guest",
);
assert!(cmds.contains(
"write \"/Users/user/Library/Application Support/boxlite/runtimes/v0.6.0/boxlite-guest\" /boxlite/bin/boxlite-guest\n"
));
}
}