use std::ffi::OsString;
use std::fs;
use std::io;
use std::path::{Component, Path};
use std::time::{Duration, SystemTime};
pub const PUT_TMP_PREFIX: &str = ".tmp.";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnsafeCacheRoot {
FilesystemRoot,
Empty,
TooShallow,
HomeDirectory,
}
impl core::fmt::Display for UnsafeCacheRoot {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
UnsafeCacheRoot::FilesystemRoot => "path is a filesystem root",
UnsafeCacheRoot::Empty => "path is empty",
UnsafeCacheRoot::TooShallow => "path is too shallow to be a cache dir",
UnsafeCacheRoot::HomeDirectory => "path is the home directory",
};
f.write_str(s)
}
}
impl std::error::Error for UnsafeCacheRoot {}
pub fn guard_cache_root(root: &Path) -> Result<(), UnsafeCacheRoot> {
guard_cache_root_with_home(root, std::env::var_os("HOME"))
}
fn guard_cache_root_with_home(root: &Path, home: Option<OsString>) -> Result<(), UnsafeCacheRoot> {
if root.as_os_str().is_empty() {
return Err(UnsafeCacheRoot::Empty);
}
let mut normal = 0usize;
let mut only_root_prefix = true;
for c in root.components() {
match c {
Component::Normal(_) => {
normal += 1;
only_root_prefix = false;
}
Component::CurDir | Component::ParentDir => only_root_prefix = false,
Component::RootDir | Component::Prefix(_) => {}
}
}
if only_root_prefix {
return Err(UnsafeCacheRoot::FilesystemRoot);
}
if let Some(home) = home {
if !home.is_empty() && Path::new(&home) == root {
return Err(UnsafeCacheRoot::HomeDirectory);
}
}
if normal < 2 {
return Err(UnsafeCacheRoot::TooShallow);
}
Ok(())
}
pub fn clean_cache(root: impl AsRef<Path>) -> io::Result<()> {
let root = root.as_ref();
guard_cache_root(root).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
match fs::remove_dir_all(root) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
fs::create_dir_all(root)
}
pub const ORPHAN_MIN_AGE: Duration = Duration::from_secs(3600);
pub fn sweep_tmp_orphans(cache_dir: impl AsRef<Path>, older_than: Duration) -> io::Result<usize> {
let dir = cache_dir.as_ref();
let entries = match fs::read_dir(dir) {
Ok(rd) => rd,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0),
Err(e) => return Err(e),
};
let now = SystemTime::now();
let mut removed = 0usize;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with(PUT_TMP_PREFIX) {
continue;
}
let Ok(ft) = entry.file_type() else { continue };
if !ft.is_file() {
continue;
}
let Ok(meta) = entry.metadata() else { continue };
let Ok(mtime) = meta.modified() else { continue };
let age = now.duration_since(mtime).unwrap_or_default();
if age < older_than {
continue; }
if fs::remove_file(entry.path()).is_ok() {
removed += 1;
}
}
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn scratch(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("cl-182-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn sweep_removes_only_old_tmp_files_not_content_or_dirs() {
let dir = scratch("matrix");
fs::write(dir.join(".tmp.abc.123.0"), b"partial").unwrap();
fs::write(dir.join(".tmp.def.456.1"), b"partial2").unwrap();
fs::write(dir.join("a1b2c3deadbeef"), b"real cached artifact").unwrap();
fs::write(dir.join("not-a-temp.txt"), b"keep").unwrap();
fs::create_dir_all(dir.join(".tmp.adir.789.2")).unwrap();
let n = sweep_tmp_orphans(&dir, Duration::ZERO).unwrap();
assert_eq!(n, 2, "exactly the two .tmp.* regular files removed");
assert!(!dir.join(".tmp.abc.123.0").exists());
assert!(!dir.join(".tmp.def.456.1").exists());
assert!(dir.join("a1b2c3deadbeef").exists(), "content entry safe");
assert!(dir.join("not-a-temp.txt").exists(), "non-tmp file safe");
assert!(
dir.join(".tmp.adir.789.2").is_dir(),
".tmp.-prefixed directory must NOT be removed (not a file)"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn sweep_leaves_fresh_tmp_in_flight_writes_untouched() {
let dir = scratch("inflight");
fs::write(dir.join(".tmp.live.999.0"), b"being written").unwrap();
let n = sweep_tmp_orphans(&dir, ORPHAN_MIN_AGE).unwrap();
assert_eq!(n, 0, "fresh temp under threshold must not be swept");
assert!(
dir.join(".tmp.live.999.0").exists(),
"an in-flight put's fresh temp survives the sweep"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn sweep_missing_dir_is_ok_zero() {
let missing = std::env::temp_dir().join(format!("cl-182-absent-{}", std::process::id()));
let _ = fs::remove_dir_all(&missing);
assert_eq!(sweep_tmp_orphans(&missing, Duration::ZERO).unwrap(), 0);
}
#[test]
fn dangerous_roots_are_refused() {
assert_eq!(
guard_cache_root(Path::new("/")),
Err(UnsafeCacheRoot::FilesystemRoot)
);
assert_eq!(guard_cache_root(Path::new("")), Err(UnsafeCacheRoot::Empty));
assert_eq!(
guard_cache_root(Path::new("/tmp")),
Err(UnsafeCacheRoot::TooShallow)
);
}
#[test]
fn home_directory_is_refused() {
let home = "/home/somebody/account";
assert_eq!(
guard_cache_root_with_home(Path::new(home), Some(OsString::from(home))),
Err(UnsafeCacheRoot::HomeDirectory)
);
assert!(
guard_cache_root_with_home(Path::new(home), Some(OsString::from("/home/someone-else")))
.is_ok()
);
assert!(guard_cache_root_with_home(Path::new(home), None).is_ok());
}
#[test]
fn reasonable_cache_dir_is_accepted() {
assert!(guard_cache_root(Path::new("/home/u/.cache/cargoless-cas")).is_ok());
assert!(guard_cache_root(Path::new("/var/lib/tf/cache")).is_ok());
}
#[test]
fn clean_wipes_contents_but_leaves_an_empty_dir() {
let mut root: PathBuf = std::env::temp_dir();
root.push(format!("cargoless-cas-clean-{}", std::process::id()));
root.push("cache");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
fs::write(root.join("deadbeef"), b"cached artifact").unwrap();
assert!(root.join("deadbeef").exists());
clean_cache(&root).unwrap();
assert!(root.exists(), "cache dir is recreated, ready for reuse");
assert!(!root.join("deadbeef").exists(), "cached entries are gone");
assert_eq!(fs::read_dir(&root).unwrap().count(), 0);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn clean_on_missing_root_is_ok() {
let mut root: PathBuf = std::env::temp_dir();
root.push(format!("cargoless-cas-clean-absent-{}", std::process::id()));
root.push("nested-cache");
let _ = fs::remove_dir_all(&root);
assert!(clean_cache(&root).is_ok());
assert!(root.exists());
let _ = fs::remove_dir_all(&root);
}
#[test]
fn clean_refuses_dangerous_root_with_invalid_input() {
let err = clean_cache("/").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
}