use std::path::{Path, PathBuf};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
pub fn is_safe_owned_dir(path: &Path, uid: u32) -> bool {
let Ok(md) = std::fs::symlink_metadata(path) else {
return false;
};
md.is_dir() && md.uid() == uid && (md.mode() & 0o077) == 0
}
pub fn ensure_safe_dir(dir: &Path, uid: u32) -> bool {
if let Some(parent) = dir.parent() {
let _ = std::fs::create_dir_all(parent);
}
match std::fs::DirBuilder::new().mode(0o700).create(dir) {
Ok(()) => return true,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(_) => return false,
}
if let Ok(md) = std::fs::symlink_metadata(dir)
&& md.is_dir()
&& md.uid() == uid
&& md.mode() & 0o077 != 0
{
let _ = std::fs::set_permissions(dir, PermissionsExt::from_mode(0o700));
}
is_safe_owned_dir(dir, uid)
}
pub fn resolve_root(uid: u32, fallback: Option<PathBuf>) -> Option<PathBuf> {
let abs = |v: std::ffi::OsString| -> Option<PathBuf> {
let p = PathBuf::from(v);
p.is_absolute().then_some(p)
};
let base = std::env::var_os("XDG_CACHE_HOME")
.and_then(abs)
.or_else(|| {
std::env::var_os("HOME")
.and_then(abs)
.map(|h| h.join(".cache"))
})
.or(fallback)?;
let onelf = base.join("onelf");
ensure_safe_dir(&onelf, uid).then_some(onelf)
}
pub fn pkg_dir(root: &Path, package_id: &str) -> PathBuf {
root.join("pkg").join(package_id)
}
pub fn ready_marker(root: &Path, package_id: &str) -> PathBuf {
root.join("pkg").join(format!(".{package_id}.ready"))
}
pub fn lock_path(root: &Path, package_id: &str) -> PathBuf {
root.join("lock").join(package_id)
}
pub fn extract_lock_path(root: &Path, package_id: &str) -> PathBuf {
root.join("lock").join(format!("{package_id}.extract"))
}
pub fn meta_path(root: &Path, package_id: &str) -> PathBuf {
root.join("meta").join(package_id)
}
pub fn cas_lock_path(root: &Path) -> PathBuf {
root.join("lock").join("cas")
}
#[cfg(test)]
mod tests {
use super::*;
fn unique(tag: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
std::env::temp_dir().join(format!(
"onelf-{tag}-{}-{}",
std::process::id(),
SEQ.fetch_add(1, Ordering::Relaxed)
))
}
fn uid() -> u32 {
let p = unique("uidprobe");
std::fs::write(&p, b"").unwrap();
let u = std::fs::symlink_metadata(&p).unwrap().uid();
let _ = std::fs::remove_file(&p);
u
}
#[test]
fn safe_dir_rules() {
let me = uid();
let dir = unique("layout");
let _ = std::fs::remove_dir_all(&dir);
assert!(ensure_safe_dir(&dir, me), "fresh dir is created 0700");
assert!(is_safe_owned_dir(&dir, me));
std::fs::set_permissions(&dir, PermissionsExt::from_mode(0o755)).unwrap();
assert!(!is_safe_owned_dir(&dir, me), "0755 is not owner-private");
assert!(ensure_safe_dir(&dir, me), "our own dir is tightened back");
assert!(is_safe_owned_dir(&dir, me));
assert!(
!is_safe_owned_dir(&dir, me.wrapping_add(1)),
"another uid's directory is refused, never chmod'd"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn missing_and_non_directory_are_unsafe() {
let me = uid();
assert!(!is_safe_owned_dir(Path::new("/nonexistent/onelf/xyz"), me));
let f = unique("layout-file");
std::fs::write(&f, b"x").unwrap();
assert!(!is_safe_owned_dir(&f, me), "a regular file is not a dir");
let _ = std::fs::remove_file(&f);
}
#[test]
fn relative_env_roots_are_not_trusted() {
let me = uid();
let fallback = unique("fb");
let _ = std::fs::remove_dir_all(&fallback);
std::fs::create_dir_all(&fallback).unwrap();
let prev_xdg = std::env::var_os("XDG_CACHE_HOME");
let prev_home = std::env::var_os("HOME");
unsafe {
std::env::set_var("XDG_CACHE_HOME", "relative/path");
std::env::remove_var("HOME");
}
let root = resolve_root(me, Some(fallback.clone()));
unsafe {
match prev_xdg {
Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
None => std::env::remove_var("XDG_CACHE_HOME"),
}
if let Some(v) = prev_home {
std::env::set_var("HOME", v);
}
}
assert_eq!(root, Some(fallback.join("onelf")));
let _ = std::fs::remove_dir_all(&fallback);
}
#[test]
fn layout_paths_are_stable() {
let root = Path::new("/c/onelf");
assert_eq!(pkg_dir(root, "ab"), Path::new("/c/onelf/pkg/ab"));
assert_eq!(
ready_marker(root, "ab"),
Path::new("/c/onelf/pkg/.ab.ready")
);
assert_eq!(lock_path(root, "ab"), Path::new("/c/onelf/lock/ab"));
assert_eq!(
extract_lock_path(root, "ab"),
Path::new("/c/onelf/lock/ab.extract")
);
assert_eq!(meta_path(root, "ab"), Path::new("/c/onelf/meta/ab"));
}
}