use std::fs;
use std::io::{self, Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use onelf_format::{EntryKind, symlink_target_within_root};
use crate::loader::{self, PackageData};
use crate::paths::open_lock_inheritable;
use onelf_format::cache_layout as layout;
static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
fn file_hashes_to(path: &Path, expected: &[u8; 32]) -> bool {
let Ok(mut f) = fs::File::open(path) else {
return false;
};
let mut hasher = blake3::Hasher::new();
let mut buf = [0u8; 64 * 1024];
loop {
match f.read(&mut buf) {
Ok(0) => break,
Ok(n) => hasher.update(&buf[..n]),
Err(_) => return false,
};
}
hasher.finalize().as_bytes() == expected
}
fn cache_dir() -> Option<PathBuf> {
onelf_format::cache_layout::resolve_root(
rustix::process::getuid().as_raw(),
crate::paths::private_dir(),
)
}
pub fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn extract_direct(pkg: &mut PackageData, target_dir: &Path) -> io::Result<()> {
let manifest = &pkg.manifest;
for (i, entry) in manifest.entries.iter().enumerate() {
if entry.kind == EntryKind::Dir {
let rel = manifest.validated_entry_path(i)?;
if rel.as_os_str().is_empty() {
continue;
}
fs::create_dir_all(target_dir.join(&rel))?;
}
}
for (i, entry) in manifest.entries.iter().enumerate() {
if entry.kind != EntryKind::File {
continue;
}
let rel = manifest.validated_entry_path(i)?;
let out_path = target_dir.join(&rel);
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let data =
loader::read_verified_entry(&mut pkg.file, &pkg.footer, entry, pkg.dict.as_deref())?;
let mut f = fs::File::create(&out_path)?;
f.write_all(&data)?;
f.set_permissions(fs::Permissions::from_mode(entry.mode & 0o777))?;
}
for (i, entry) in manifest.entries.iter().enumerate() {
if entry.kind != EntryKind::Symlink {
continue;
}
let rel = manifest.validated_entry_path(i)?;
let target = manifest.get_string(entry.symlink_target);
if !symlink_target_within_root(&rel, target) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"onelf: symlink target escapes package root",
));
}
let link_path = target_dir.join(&rel);
if let Some(parent) = link_path.parent() {
fs::create_dir_all(parent)?;
}
if link_path.symlink_metadata().is_ok() {
fs::remove_file(&link_path)?;
}
std::os::unix::fs::symlink(target, &link_path)?;
}
Ok(())
}
pub fn ensure_extracted(pkg: &mut PackageData) -> io::Result<(PathBuf, fs::File)> {
use rustix::fs::FlockOperation;
let base = cache_dir().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"onelf: no safe cache directory (set HOME or XDG_RUNTIME_DIR)",
)
})?;
let package_id = hex(&pkg.manifest.header.package_id);
let pkg_parent = base.join("pkg");
let pkg_dir = pkg_parent.join(&package_id);
let cas_dir = base.join("cas");
let lock_dir = base.join("lock");
let meta_dir = base.join("meta");
let ready_path = layout::ready_marker(&base, &package_id);
let is_complete = || pkg_dir.exists() && ready_path.exists();
fs::create_dir_all(&lock_dir)?;
let lock_path = layout::lock_path(&base, &package_id);
let lock_file = open_lock_inheritable(&lock_path)?;
rustix::fs::flock(&lock_file, FlockOperation::LockShared)
.map_err(|e| io::Error::other(format!("flock: {e}")))?;
if is_complete() {
touch_meta(&meta_dir, &package_id);
return Ok((pkg_dir, lock_file));
}
let extract_path = layout::extract_lock_path(&base, &package_id);
let extract_lock = fs::File::create(&extract_path)?;
rustix::fs::flock(&extract_lock, FlockOperation::LockExclusive)
.map_err(|e| io::Error::other(format!("flock: {e}")))?;
if !is_complete() {
fs::create_dir_all(&cas_dir)?;
fs::create_dir_all(&pkg_parent)?;
let _ = fs::remove_dir_all(&pkg_dir);
let _ = fs::remove_file(&ready_path);
let tmp_dir = pkg_parent.join(format!(".{package_id}.tmp"));
let _ = fs::remove_dir_all(&tmp_dir);
fs::create_dir_all(&tmp_dir)?;
let cas_lock = open_lock_inheritable(&layout::cas_lock_path(&base))?;
rustix::fs::flock(&cas_lock, FlockOperation::LockShared)
.map_err(|e| io::Error::other(format!("flock: {e}")))?;
if let Err(e) = extract_to_cas(pkg, &cas_dir, &tmp_dir) {
let _ = fs::remove_dir_all(&tmp_dir);
return Err(e);
}
drop(cas_lock);
if let Err(e) = fs::rename(&tmp_dir, &pkg_dir) {
let _ = fs::remove_dir_all(&tmp_dir);
return Err(e);
}
if let Err(e) = fs::File::create(&ready_path) {
let _ = fs::remove_dir_all(&pkg_dir);
return Err(e);
}
}
touch_meta(&meta_dir, &package_id);
drop(extract_lock);
Ok((pkg_dir, lock_file))
}
fn extract_to_cas(pkg: &mut PackageData, cas_dir: &Path, pkg_dir: &Path) -> io::Result<()> {
let manifest = &pkg.manifest;
for (i, entry) in manifest.entries.iter().enumerate() {
if entry.kind == EntryKind::Dir {
let rel = manifest.validated_entry_path(i)?;
if rel.as_os_str().is_empty() {
continue;
}
fs::create_dir_all(pkg_dir.join(&rel))?;
}
}
for (i, entry) in manifest.entries.iter().enumerate() {
if entry.kind != EntryKind::File {
continue;
}
let rel = manifest.validated_entry_path(i)?;
let hash_hex = hex(&entry.content_hash);
let shard = &hash_hex[..2];
let cas_shard_dir = cas_dir.join(shard);
let cas_path = cas_shard_dir.join(&hash_hex);
let reuse = cas_path.exists() && file_hashes_to(&cas_path, &entry.content_hash);
if !reuse {
fs::create_dir_all(&cas_shard_dir)?;
let data = loader::read_verified_entry(
&mut pkg.file,
&pkg.footer,
entry,
pkg.dict.as_deref(),
)?;
let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);
let tmp_path =
cas_shard_dir.join(format!(".{hash_hex}.{}.{seq}.tmp", std::process::id()));
let write = (|| -> io::Result<()> {
let mut f = fs::File::create(&tmp_path)?;
f.write_all(&data)?;
f.set_permissions(fs::Permissions::from_mode(entry.mode & 0o777))?;
Ok(())
})();
if let Err(e) = write {
let _ = fs::remove_file(&tmp_path);
return Err(e);
}
fs::rename(&tmp_path, &cas_path)?;
}
let link_path = pkg_dir.join(&rel);
if let Some(parent) = link_path.parent() {
fs::create_dir_all(parent)?;
}
if link_path.symlink_metadata().is_ok() {
fs::remove_file(&link_path)?;
}
fs::hard_link(&cas_path, &link_path)?;
}
for (i, entry) in manifest.entries.iter().enumerate() {
if entry.kind != EntryKind::Symlink {
continue;
}
let rel = manifest.validated_entry_path(i)?;
let target = manifest.get_string(entry.symlink_target);
if !symlink_target_within_root(&rel, target) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"onelf: symlink target escapes package root",
));
}
let link_path = pkg_dir.join(&rel);
if let Some(parent) = link_path.parent() {
fs::create_dir_all(parent)?;
}
if link_path.symlink_metadata().is_ok() {
fs::remove_file(&link_path)?;
}
std::os::unix::fs::symlink(target, &link_path)?;
}
Ok(())
}
fn touch_meta(meta_dir: &Path, package_id: &str) {
let _ = fs::create_dir_all(meta_dir);
let meta_path = meta_dir.join(package_id);
let _ = fs::File::create(&meta_path);
}
pub fn remove_package(base: &Path, package_id: &str) {
let _ = fs::remove_file(layout::ready_marker(base, package_id));
let _ = fs::remove_dir_all(layout::pkg_dir(base, package_id));
let _ = fs::remove_file(layout::meta_path(base, package_id));
let _ = fs::remove_file(layout::extract_lock_path(base, package_id));
}
pub fn collect_cas(base: &Path) -> u64 {
use std::os::unix::fs::MetadataExt;
let Ok(lock) = open_lock_inheritable(&layout::cas_lock_path(base)) else {
return 0;
};
if rustix::fs::flock(&lock, rustix::fs::FlockOperation::NonBlockingLockExclusive).is_err() {
return 0;
}
let Ok(shards) = fs::read_dir(base.join("cas")) else {
return 0;
};
let mut reclaimed = 0u64;
for shard in shards.flatten() {
let Ok(blobs) = fs::read_dir(shard.path()) else {
continue;
};
for blob in blobs.flatten() {
let Ok(md) = blob.metadata() else { continue };
if !md.is_file() || md.nlink() > 1 {
continue;
}
let len = md.len();
if fs::remove_file(blob.path()).is_ok() {
reclaimed += len;
}
}
}
reclaimed
}
pub fn auto_gc(base: &Path, max_age_secs: u64, current_pkg_id: &str) {
let meta_dir = base.join("meta");
let entries = match fs::read_dir(&meta_dir) {
Ok(e) => e,
Err(_) => return,
};
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(_) => return,
};
let mut removed = 0u32;
for entry in entries.flatten() {
if removed >= 5 {
break;
}
let name = entry.file_name();
let id = name.to_string_lossy();
if id == current_pkg_id {
continue;
}
let mtime = match entry.metadata().and_then(|m| m.modified()) {
Ok(t) => t
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
Err(_) => continue,
};
if now.saturating_sub(mtime) > max_age_secs && try_remove_locked(base, &id) {
removed += 1;
}
}
if removed > 0 {
collect_cas(base);
}
}
fn try_remove_locked(base: &Path, id: &str) -> bool {
let lock_path = layout::lock_path(base, id);
let f = match fs::File::open(&lock_path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
remove_package(base, id);
return true;
}
Err(_) => return false,
};
if rustix::fs::flock(&f, rustix::fs::FlockOperation::NonBlockingLockExclusive).is_err() {
return false; }
remove_package(base, id);
true
}
pub fn base_dir() -> Option<PathBuf> {
cache_dir()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_hashes_to_detects_poisoning() {
let dir = std::env::temp_dir().join(format!("onelf-cas-test-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let path = dir.join("blob");
let good = b"the real library bytes";
fs::write(&path, good).unwrap();
let good_hash = *blake3::hash(good).as_bytes();
assert!(file_hashes_to(&path, &good_hash));
fs::write(&path, b"malicious replacement").unwrap();
assert!(!file_hashes_to(&path, &good_hash));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn collect_cas_respects_links_and_the_store_lock() {
use rustix::fs::FlockOperation;
let base = std::env::temp_dir().join(format!("onelf-cas-gc-{}", std::process::id()));
let _ = fs::remove_dir_all(&base);
let shard = base.join("cas/ab");
let pkg = base.join("pkg/somepkg");
fs::create_dir_all(&shard).unwrap();
fs::create_dir_all(&pkg).unwrap();
fs::create_dir_all(base.join("lock")).unwrap();
let linked = shard.join("aaaa");
fs::write(&linked, b"still in use").unwrap();
fs::hard_link(&linked, pkg.join("f")).unwrap();
let orphan = shard.join("bbbb");
fs::write(&orphan, b"nobody wants me").unwrap();
let extracting = open_lock_inheritable(&layout::cas_lock_path(&base)).unwrap();
rustix::fs::flock(&extracting, FlockOperation::LockShared).unwrap();
assert_eq!(collect_cas(&base), 0, "must not collect mid-extraction");
assert!(orphan.exists(), "an orphan is spared while extraction runs");
drop(extracting);
let reclaimed = collect_cas(&base);
assert!(linked.exists(), "a hardlinked blob must survive");
assert!(!orphan.exists(), "an unreferenced blob is reclaimed");
assert_eq!(reclaimed, b"nobody wants me".len() as u64);
let _ = fs::remove_dir_all(&base);
}
#[test]
fn gc_skips_locked_and_removes_idle() {
use std::os::unix::fs::PermissionsExt;
let base = std::env::temp_dir().join(format!("onelf-gc-test-{}", std::process::id()));
let _ = fs::remove_dir_all(&base);
for sub in ["pkg", "meta", "lock"] {
fs::create_dir_all(base.join(sub)).unwrap();
}
let old = std::fs::FileTimes::new()
.set_modified(std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1));
for id in ["locked", "idle"] {
fs::create_dir_all(base.join("pkg").join(id)).unwrap();
fs::write(base.join("pkg").join(id).join("f"), b"x").unwrap();
fs::File::create(base.join("lock").join(id)).unwrap();
let m = fs::File::create(base.join("meta").join(id)).unwrap();
m.set_times(old).unwrap();
let _ =
fs::set_permissions(base.join("meta").join(id), PermissionsExt::from_mode(0o644));
}
let held = fs::File::open(base.join("lock").join("locked")).unwrap();
rustix::fs::flock(&held, rustix::fs::FlockOperation::LockExclusive).unwrap();
auto_gc(&base, 0, "none");
assert!(
base.join("pkg").join("locked").exists(),
"running package must survive GC"
);
assert!(
!base.join("pkg").join("idle").exists(),
"idle over-age package must be removed"
);
let _ = fs::remove_dir_all(&base);
}
}