use std::path::Path;
use a3s_box_core::error::{BoxError, Result};
pub fn overlay_mount(lower: &Path, upper: &Path, work: &Path, merged: &Path) -> Result<()> {
for path in [lower, upper, work] {
if path.to_string_lossy().contains(',') {
return Err(BoxError::BuildError(format!(
"overlay path contains a comma, which overlayfs options cannot express: {}",
path.display()
)));
}
}
let base_options = overlay_options(lower, upper, work, false);
#[cfg(target_os = "linux")]
{
use std::ffi::CString;
let metadata_options = overlay_options(lower, upper, work, true);
let options = if unsafe { libc::geteuid() } == 0 {
vec![(&metadata_options, true), (&base_options, false)]
} else {
vec![(&base_options, false)]
};
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 mut failures = Vec::new();
for &(options, metadata_copy) in &options {
let data = CString::new(options.as_str()).map_err(|error| {
BoxError::BuildError(format!("Invalid overlay mount options: {error}"))
})?;
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(),
metadata_copy,
"Overlay mounted via mount(2)"
);
return Ok(());
}
failures.push(format!(
"mount(2), metacopy={metadata_copy}: {}",
std::io::Error::last_os_error()
));
}
tracing::debug!(
errors = ?failures,
"mount(2) failed, trying mount command"
);
for &(options, metadata_copy) in &options {
match std::process::Command::new("mount")
.args(["-t", "overlay", "overlay", "-o", options])
.arg(merged)
.status()
{
Ok(status) if status.success() => {
tracing::debug!(
lower = %lower.display(),
merged = %merged.display(),
metadata_copy,
"Overlay mounted via mount command"
);
return Ok(());
}
Ok(status) => {
failures.push(format!("mount command, metacopy={metadata_copy}: {status}"))
}
Err(error) => {
failures.push(format!("mount command, metacopy={metadata_copy}: {error}"))
}
}
}
Err(BoxError::BuildError(format!(
"Failed to mount overlayfs at {}: {}",
merged.display(),
failures.join("; ")
)))
}
#[cfg(not(target_os = "linux"))]
{
let _ = (lower, upper, work, merged, base_options);
Err(BoxError::BuildError(
"Overlayfs is only supported on Linux".to_string(),
))
}
}
fn overlay_options(lower: &Path, upper: &Path, work: &Path, metadata_copy: bool) -> String {
let mut options = format!(
"lowerdir={},upperdir={},workdir={}",
lower.display(),
upper.display(),
work.display()
);
if metadata_copy {
options.push_str(",metacopy=on");
}
options
}
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"))]
#[allow(dead_code)]
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());
}
#[test]
fn test_overlay_mount_rejects_comma_in_mount_option_paths() {
let tmp = tempfile::TempDir::new().unwrap();
let lower = tmp.path().join("lower,with-comma");
let upper = tmp.path().join("upper");
let work = tmp.path().join("work");
let merged = tmp.path().join("merged");
let err = overlay_mount(&lower, &upper, &work, &merged).unwrap_err();
assert!(err.to_string().contains("contains a comma"));
assert!(err.to_string().contains("lower,with-comma"));
}
#[test]
fn metadata_copy_option_is_explicit() {
let lower = Path::new("/cache/lower");
let upper = Path::new("/box/upper");
let work = Path::new("/box/work");
assert_eq!(
overlay_options(lower, upper, work, false),
"lowerdir=/cache/lower,upperdir=/box/upper,workdir=/box/work"
);
assert_eq!(
overlay_options(lower, upper, work, true),
"lowerdir=/cache/lower,upperdir=/box/upper,workdir=/box/work,metacopy=on"
);
}
#[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();
}
}