use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use std::fs;
use std::path::Path;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::process::Command;
#[cfg(target_os = "linux")]
#[allow(dead_code)]
pub fn mount_overlayfs_from_layers(
lower_dirs: &[PathBuf],
upper_dir: &Path,
work_dir: &Path,
target_dir: &Path,
) -> BoxliteResult<()> {
if lower_dirs.is_empty() {
return Err(BoxliteError::Storage(
"Cannot mount overlayfs with no lower directories".into(),
));
}
let lowerdir = lower_dirs
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(":");
let mount_options = format!(
"lowerdir={},upperdir={},workdir={}",
lowerdir,
upper_dir.display(),
work_dir.display()
);
tracing::debug!("Mounting overlayfs with options: {}", mount_options);
let output = Command::new("mount")
.args([
"-t",
"overlay",
"overlay",
"-o",
&mount_options,
target_dir
.to_str()
.ok_or_else(|| BoxliteError::Storage("Invalid target path".into()))?,
])
.output()
.map_err(|e| BoxliteError::Storage(format!("Failed to execute mount command: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BoxliteError::Storage(format!(
"Failed to mount overlayfs: {}",
stderr
)));
}
tracing::info!("Overlayfs mounted at {}", target_dir.display());
Ok(())
}
#[cfg(target_os = "linux")]
#[allow(dead_code)]
pub fn unmount_overlayfs(mount_point: &Path) -> BoxliteResult<()> {
let output = Command::new("umount")
.arg(mount_point)
.output()
.map_err(|e| {
BoxliteError::Storage(format!(
"Failed to execute umount for {}: {}",
mount_point.display(),
e
))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BoxliteError::Storage(format!(
"Failed to unmount overlayfs at {}: {}",
mount_point.display(),
stderr
)));
}
tracing::debug!("Unmounted overlayfs at {}", mount_point.display());
Ok(())
}
#[cfg(not(target_os = "linux"))]
#[allow(dead_code)]
pub fn unmount_overlayfs(_mount_point: &Path) -> BoxliteResult<()> {
Ok(()) }
#[allow(dead_code)]
pub fn process_whiteouts(dir: &Path) -> BoxliteResult<()> {
fn process_dir_recursive(dir: &Path) -> BoxliteResult<()> {
let entries: Vec<_> = fs::read_dir(dir)
.map_err(|e| {
BoxliteError::Storage(format!("Failed to read directory {}: {}", dir.display(), e))
})?
.filter_map(|e| e.ok())
.collect();
for entry in entries {
let path = entry.path();
let filename = entry.file_name();
let filename_str = filename.to_string_lossy();
if let Some(target_name) = filename_str.strip_prefix(".wh.") {
if target_name == ".wh..opq" {
let _ = fs::remove_file(&path);
} else {
let target_path = dir.join(target_name);
if target_path.exists() {
if target_path.is_dir() {
let _ = fs::remove_dir_all(&target_path);
} else {
let _ = fs::remove_file(&target_path);
}
tracing::debug!("Processed whiteout: removed {}", target_path.display());
}
let _ = fs::remove_file(&path);
}
} else if path.is_dir() {
process_dir_recursive(&path)?;
}
}
Ok(())
}
process_dir_recursive(dir)
}
pub fn fix_rootfs_permissions(rootfs: &Path) -> BoxliteResult<()> {
use std::fs;
use std::os::unix::fs::PermissionsExt;
tracing::info!(
"Setting per-file xattr for rootfs permissions on {}",
rootfs.display()
);
fn set_xattr_recursive(path: &Path, depth: usize) -> BoxliteResult<usize> {
let metadata = match fs::symlink_metadata(path) {
Ok(m) => m,
Err(e) => {
tracing::debug!("Skipping {}: {}", path.display(), e);
return Ok(0);
}
};
let mut count = 0;
if metadata.file_type().is_symlink() {
return Ok(0);
}
let mode = metadata.permissions().mode() & 0o7777;
let xattr_value = format!("0:0:{:04o}", mode);
let needs_write = (mode & 0o200) == 0;
if needs_write {
let mut temp_perms = metadata.permissions();
temp_perms.set_mode(mode | 0o200); let _ = fs::set_permissions(path, temp_perms);
}
match xattr::set(
path,
"user.containers.override_stat",
xattr_value.as_bytes(),
) {
Ok(_) => {
count += 1;
if depth < 2 {
tracing::debug!(
"Set xattr on {}: {} (mode: {:o})",
path.display(),
xattr_value,
mode
);
}
}
Err(e) => {
tracing::trace!("Failed to set xattr on {}: {}", path.display(), e);
}
}
if needs_write {
let mut orig_perms = metadata.permissions();
orig_perms.set_mode(mode);
let _ = fs::set_permissions(path, orig_perms);
}
if metadata.is_dir()
&& let Ok(entries) = fs::read_dir(path)
{
for entry in entries.filter_map(|e| e.ok()) {
count += set_xattr_recursive(&entry.path(), depth + 1)?;
}
}
Ok(count)
}
let count = set_xattr_recursive(rootfs, 0)?;
tracing::info!("✅ Per-file xattr set for {} files in rootfs", count);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_process_whiteouts_removes_target_file() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
fs::write(dir.join("file.txt"), "content").unwrap();
fs::write(dir.join(".wh.file.txt"), "").unwrap();
process_whiteouts(dir).unwrap();
assert!(!dir.join("file.txt").exists());
assert!(!dir.join(".wh.file.txt").exists());
}
#[test]
fn test_process_whiteouts_removes_opaque_marker() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
fs::write(dir.join(".wh..wh..opq"), "").unwrap();
process_whiteouts(dir).unwrap();
assert!(!dir.join(".wh..wh..opq").exists());
}
}