use std::path::{Path, PathBuf};
use std::os::unix::fs::DirBuilderExt;
fn uid() -> u32 {
rustix::process::getuid().as_raw()
}
fn is_safe_owned_dir(path: &Path) -> bool {
onelf_format::cache_layout::is_safe_owned_dir(path, uid())
}
pub fn private_dir() -> Option<PathBuf> {
if let Some(x) = std::env::var_os("XDG_RUNTIME_DIR") {
let p = PathBuf::from(x);
if is_safe_owned_dir(&p) {
return Some(p);
}
}
let p = PathBuf::from(format!("/tmp/onelf-{}", uid()));
match std::fs::DirBuilder::new().mode(0o700).create(&p) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(_) => return None,
}
is_safe_owned_dir(&p).then_some(p)
}
pub(crate) fn open_lock_inheritable(path: &Path) -> std::io::Result<std::fs::File> {
use rustix::fs::{Mode, OFlags};
let fd = rustix::fs::open(path, OFlags::CREATE | OFlags::RDWR, Mode::RUSR | Mode::WUSR)?;
Ok(std::fs::File::from(fd))
}
fn mountpoint_lock_path(base: &Path, dir_name: &str) -> PathBuf {
base.join(format!(".{dir_name}.lock"))
}
pub struct Mountpoint {
pub path: PathBuf,
_lock: std::fs::File,
}
impl Mountpoint {
pub fn path(&self) -> &Path {
&self.path
}
}
pub fn create_mountpoint(package_name: &str, package_id: &[u8; 32]) -> Option<Mountpoint> {
use rustix::fs::FlockOperation;
let name_prefix: String = package_name.chars().take(6).collect();
let hash_suffix = crate::cache::hex(&package_id[0..4]);
let dir_name = format!("onelf-{name_prefix}-{hash_suffix}");
let base = private_dir()?;
let lock_path = mountpoint_lock_path(&base, &dir_name);
let lock = match open_lock_inheritable(&lock_path) {
Ok(f) => f,
Err(e) => {
eprintln!("onelf-rt: cannot open {}: {e}", lock_path.display());
return None;
}
};
if let Err(e) = rustix::fs::flock(&lock, FlockOperation::LockShared) {
eprintln!("onelf-rt: cannot lock {}: {e}", lock_path.display());
return None;
}
let path = base.join(&dir_name);
if let Err(e) = std::fs::create_dir_all(&path) {
eprintln!("onelf-rt: cannot create {}: {e}", path.display());
return None;
}
Some(Mountpoint { path, _lock: lock })
}
const RECLAIM_GRACE_SECS: u64 = 3600;
fn older_than_grace(path: &Path) -> bool {
let Ok(md) = std::fs::symlink_metadata(path) else {
return false;
};
let Ok(modified) = md.modified() else {
return false;
};
modified
.elapsed()
.map(|age| age.as_secs() >= RECLAIM_GRACE_SECS)
.unwrap_or(false)
}
pub fn sweep_stale_mountpoints() {
let Some(base) = private_dir() else {
return;
};
sweep_in(&base);
}
fn sweep_in(base: &Path) {
let Ok(entries) = std::fs::read_dir(base) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
match name
.strip_prefix('.')
.and_then(|rest| rest.strip_suffix(".lock"))
{
Some(owner) if owner.starts_with("onelf-") => reclaim_orphan_lock(base, owner),
_ if name.starts_with("onelf-") => reclaim_mountpoint(base, name, &entry.path()),
_ => {}
}
}
}
fn reclaim_mountpoint(base: &Path, dir_name: &str, path: &Path) {
use rustix::fs::FlockOperation;
if !path.is_dir() {
return;
}
match std::fs::File::open(mountpoint_lock_path(base, dir_name)) {
Ok(lock) => {
if rustix::fs::flock(&lock, FlockOperation::NonBlockingLockExclusive).is_err() {
return;
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if !older_than_grace(path) {
return;
}
}
Err(_) => return,
}
let _ = std::fs::remove_dir(path);
}
fn reclaim_orphan_lock(base: &Path, dir_name: &str) {
use rustix::fs::FlockOperation;
if base.join(dir_name).exists() {
return;
}
let lock_path = mountpoint_lock_path(base, dir_name);
if !older_than_grace(&lock_path) {
return;
}
let Ok(lock) = std::fs::File::open(&lock_path) else {
return;
};
if rustix::fs::flock(&lock, FlockOperation::NonBlockingLockExclusive).is_err() {
return;
}
let _ = std::fs::remove_file(&lock_path);
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn prefers_safe_xdg_runtime_dir() {
let dir = std::env::temp_dir().join(format!("onelf-priv-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, PermissionsExt::from_mode(0o700)).unwrap();
assert!(is_safe_owned_dir(&dir));
std::fs::set_permissions(&dir, PermissionsExt::from_mode(0o755)).unwrap();
assert!(!is_safe_owned_dir(&dir));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn sweep_spares_a_claimed_mountpoint() {
use rustix::fs::FlockOperation;
let base = std::env::temp_dir().join(format!("onelf-sweep-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let claimed = base.join("onelf-held-aaaa");
let idle = base.join("onelf-idle-bbbb");
for d in [&claimed, &idle] {
std::fs::create_dir_all(d).unwrap();
}
let held = open_lock_inheritable(&mountpoint_lock_path(&base, "onelf-held-aaaa")).unwrap();
rustix::fs::flock(&held, FlockOperation::LockShared).unwrap();
drop(open_lock_inheritable(&mountpoint_lock_path(&base, "onelf-idle-bbbb")).unwrap());
sweep_in(&base);
assert!(
claimed.is_dir(),
"a claimed mountpoint must survive a sweep"
);
assert!(!idle.exists(), "an unclaimed mountpoint must be reclaimed");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn sweep_spares_a_recent_lockless_mountpoint() {
let base = std::env::temp_dir().join(format!("onelf-sweep-old-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let recent = base.join("onelf-recent-cccc");
std::fs::create_dir_all(&recent).unwrap();
sweep_in(&base);
assert!(
recent.is_dir(),
"a lockless mountpoint inside the grace period must survive"
);
let old = std::fs::FileTimes::new().set_modified(
std::time::SystemTime::now() - std::time::Duration::from_secs(RECLAIM_GRACE_SECS * 2),
);
std::fs::File::options()
.write(true)
.open(&recent)
.or_else(|_| std::fs::File::open(&recent))
.and_then(|f| f.set_times(old))
.unwrap();
sweep_in(&base);
assert!(!recent.exists(), "an aged lockless mountpoint is reclaimed");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn sweep_reclaims_orphan_lock_files() {
let base = std::env::temp_dir().join(format!("onelf-orphan-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let orphan = mountpoint_lock_path(&base, "onelf-gone-dddd");
drop(open_lock_inheritable(&orphan).unwrap());
sweep_in(&base);
assert!(orphan.exists(), "a fresh orphan is inside the grace period");
let aged = std::fs::FileTimes::new().set_modified(
std::time::SystemTime::now() - std::time::Duration::from_secs(RECLAIM_GRACE_SECS * 2),
);
std::fs::File::options()
.write(true)
.open(&orphan)
.unwrap()
.set_times(aged)
.unwrap();
sweep_in(&base);
assert!(!orphan.exists(), "an aged orphan lock is reclaimed");
let live_dir = base.join("onelf-live-eeee");
std::fs::create_dir_all(&live_dir).unwrap();
let live = mountpoint_lock_path(&base, "onelf-live-eeee");
let held = open_lock_inheritable(&live).unwrap();
rustix::fs::flock(&held, rustix::fs::FlockOperation::LockShared).unwrap();
std::fs::File::options()
.write(true)
.open(&live)
.unwrap()
.set_times(aged)
.unwrap();
sweep_in(&base);
assert!(live_dir.is_dir(), "a claimed mountpoint is kept");
assert!(live.exists(), "a claimed lock is kept");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn rejects_non_directory_and_missing() {
assert!(!is_safe_owned_dir(std::path::Path::new(
"/nonexistent/onelf/private/xyz"
)));
}
}