use std::path::Path;
use a3s_box_core::error::{BoxError, Result};
pub fn overlay_mount(lower: &Path, upper: &Path, work: &Path, merged: &Path) -> Result<()> {
let opts = format!(
"lowerdir={},upperdir={},workdir={}",
lower.display(),
upper.display(),
work.display()
);
#[cfg(target_os = "linux")]
{
use std::ffi::CString;
let source = CString::new("overlay").unwrap();
let target = CString::new(merged.to_string_lossy().as_ref())
.map_err(|e| BoxError::BuildError(format!("Invalid merged path for mount: {}", e)))?;
let fstype = CString::new("overlay").unwrap();
let data = CString::new(opts.as_str())
.map_err(|e| BoxError::BuildError(format!("Invalid overlay mount options: {}", e)))?;
let ret = unsafe {
libc::mount(
source.as_ptr(),
target.as_ptr(),
fstype.as_ptr(),
0,
data.as_ptr() as *const libc::c_void,
)
};
if ret == 0 {
tracing::debug!(
lower = %lower.display(),
merged = %merged.display(),
"Overlay mounted via mount(2)"
);
return Ok(());
}
let errno = std::io::Error::last_os_error();
tracing::debug!(
error = %errno,
"mount(2) failed, trying mount command"
);
let status = std::process::Command::new("mount")
.args(["-t", "overlay", "overlay", "-o", &opts])
.arg(merged)
.status()
.map_err(|e| BoxError::BuildError(format!("Failed to run mount command: {}", e)))?;
if status.success() {
tracing::debug!(
lower = %lower.display(),
merged = %merged.display(),
"Overlay mounted via mount command"
);
return Ok(());
}
Err(BoxError::BuildError(format!(
"Failed to mount overlayfs at {}: mount(2) returned {} and mount command exited with {}",
merged.display(),
errno,
status
)))
}
#[cfg(not(target_os = "linux"))]
{
let _ = (lower, upper, work, merged, opts);
Err(BoxError::BuildError(
"Overlayfs is only supported on Linux".to_string(),
))
}
}
pub fn overlay_unmount(merged: &Path) -> Result<()> {
#[cfg(target_os = "linux")]
{
use std::ffi::CString;
let target = CString::new(merged.to_string_lossy().as_ref())
.map_err(|e| BoxError::BuildError(format!("Invalid path for umount: {}", e)))?;
let ret = unsafe { libc::umount2(target.as_ptr(), libc::MNT_DETACH) };
if ret == 0 {
tracing::debug!(path = %merged.display(), "Overlay unmounted");
return Ok(());
}
let errno = std::io::Error::last_os_error();
let status = std::process::Command::new("umount")
.arg("-l") .arg(merged)
.status()
.map_err(|e| BoxError::BuildError(format!("Failed to run umount command: {}", e)))?;
if status.success() {
tracing::debug!(path = %merged.display(), "Overlay unmounted via umount command");
return Ok(());
}
Err(BoxError::BuildError(format!(
"Failed to unmount overlayfs at {}: umount2 returned {}, umount command exited with {}",
merged.display(),
errno,
status
)))
}
#[cfg(not(target_os = "linux"))]
{
let _ = merged;
Ok(())
}
}
#[cfg(target_os = "linux")]
pub(crate) fn is_overlay_supported() -> bool {
if let Ok(fs_list) = std::fs::read_to_string("/proc/filesystems") {
if !fs_list.contains("overlay") {
tracing::debug!("Overlay not listed in /proc/filesystems");
return false;
}
} else {
return false;
}
let tmp = match tempfile::TempDir::new() {
Ok(t) => t,
Err(_) => return false,
};
let lower = tmp.path().join("lower");
let upper = tmp.path().join("upper");
let work = tmp.path().join("work");
let merged = tmp.path().join("merged");
for dir in [&lower, &upper, &work, &merged] {
if std::fs::create_dir_all(dir).is_err() {
return false;
}
}
let ok = overlay_mount(&lower, &upper, &work, &merged).is_ok();
if ok {
let _ = overlay_unmount(&merged);
}
ok
}
#[cfg(not(target_os = "linux"))]
pub(crate) fn is_overlay_supported() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_overlay_supported_returns_bool() {
let _supported = is_overlay_supported();
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_overlay_not_supported_on_non_linux() {
assert!(!is_overlay_supported());
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_overlay_mount_fails_on_non_linux() {
let tmp = tempfile::TempDir::new().unwrap();
let result = overlay_mount(
&tmp.path().join("l"),
&tmp.path().join("u"),
&tmp.path().join("w"),
&tmp.path().join("m"),
);
assert!(result.is_err());
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_overlay_unmount_noop_on_non_linux() {
let tmp = tempfile::TempDir::new().unwrap();
assert!(overlay_unmount(tmp.path()).is_ok());
}
#[cfg(target_os = "linux")]
#[test]
fn test_overlay_mount_and_unmount() {
if !is_overlay_supported() {
return;
}
let tmp = tempfile::TempDir::new().unwrap();
let lower = tmp.path().join("lower");
let upper = tmp.path().join("upper");
let work = tmp.path().join("work");
let merged = tmp.path().join("merged");
for dir in [&lower, &upper, &work, &merged] {
std::fs::create_dir_all(dir).unwrap();
}
std::fs::write(lower.join("hello.txt"), "from lower").unwrap();
overlay_mount(&lower, &upper, &work, &merged).unwrap();
assert_eq!(
std::fs::read_to_string(merged.join("hello.txt")).unwrap(),
"from lower"
);
std::fs::write(merged.join("new.txt"), "from upper").unwrap();
assert!(upper.join("new.txt").exists());
overlay_unmount(&merged).unwrap();
}
}