use std::io;
use std::path::Path;
const SIGNATURE_LINE: &str = "Signature: 8985a1d0364e3d1e-cache-directory-tag";
const TAG_FILE_NAME: &str = "CACHEDIR.TAG";
fn tag_body() -> String {
format!(
"{SIGNATURE_LINE}\n\
# This directory contains a cache created by alef. Deleting it only costs a rebuild.\n\
# For information about cache directory tags see https://bford.info/cachedir/\n"
)
}
pub fn ensure_cache_dir(dir: &Path) -> io::Result<()> {
std::fs::create_dir_all(dir)?;
ensure_tag(dir);
Ok(())
}
pub fn ensure_cache_dir_under(root: &Path, descendant: &Path) -> io::Result<()> {
ensure_cache_dir(root)?;
ensure_cache_dir(descendant)
}
fn ensure_tag(dir: &Path) {
let tag_path = dir.join(TAG_FILE_NAME);
match std::fs::read(&tag_path) {
Ok(existing) => {
if has_valid_signature(&existing) {
return;
}
tracing::warn!(
path = %tag_path.display(),
"a file named {TAG_FILE_NAME} exists here but its first line is not the \
cache-directory-tag signature; leaving it untouched instead of overwriting \
content alef did not write -- this directory will not be recognised as a cache \
by tools that honour the tag until it is repaired or removed by hand"
);
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
if let Err(write_error) = std::fs::write(&tag_path, tag_body()) {
tracing::warn!(
path = %tag_path.display(),
error = %write_error,
"could not write {TAG_FILE_NAME}; the cache directory itself still works, \
but backup/sync tools that honour the tag will not skip it"
);
}
}
Err(error) => {
tracing::warn!(
path = %tag_path.display(),
error = %error,
"could not read the existing {TAG_FILE_NAME} to check it; leaving this cache \
directory untagged for this run rather than guessing whether it is safe to write"
);
}
}
}
fn has_valid_signature(content: &[u8]) -> bool {
let first_line = content.split(|&byte| byte == b'\n').next().unwrap_or(content);
first_line == SIGNATURE_LINE.as_bytes()
}
#[cfg(test)]
mod tests {
#[test]
fn ensure_cache_dir_under_tags_the_root_as_well_as_the_leaf() {
let base = tempfile::tempdir().expect("tempdir");
let root = base.path().join(".alef");
let leaf = root.join("sample_crate").join("hashes");
ensure_cache_dir_under(&root, &leaf).expect("root and leaf are created");
for dir in [&root, &leaf] {
let tag = dir.join(TAG_FILE_NAME);
let first_line = std::fs::read_to_string(&tag)
.unwrap_or_else(|error| panic!("no tag at {}: {error}", dir.display()))
.lines()
.next()
.unwrap_or_default()
.to_string();
assert_eq!(
first_line,
SIGNATURE_LINE,
"the tag at {} must carry the exact signature",
dir.display()
);
}
}
#[test]
fn ensure_cache_dir_alone_does_not_tag_the_parent() {
let base = tempfile::tempdir().expect("tempdir");
let root = base.path().join(".alef");
let leaf = root.join("sample_crate");
ensure_cache_dir(&leaf).expect("leaf is created");
assert!(leaf.join(TAG_FILE_NAME).is_file(), "the leaf itself must be tagged");
assert!(
!root.join(TAG_FILE_NAME).exists(),
"ensure_cache_dir must tag only the directory it is given"
);
}
use super::*;
#[test]
fn creating_a_cache_dir_writes_a_tag_with_the_exact_signature_as_its_first_line() {
let root = tempfile::tempdir().expect("temp dir");
let cache_dir = root.path().join("cache");
ensure_cache_dir(&cache_dir).expect("ensure_cache_dir must succeed");
assert!(cache_dir.is_dir(), "the cache directory itself must have been created");
let tag_content = std::fs::read_to_string(cache_dir.join(TAG_FILE_NAME)).expect("read CACHEDIR.TAG");
let first_line = tag_content.lines().next().expect("tag file must have a first line");
assert_eq!(
first_line, SIGNATURE_LINE,
"the first line must equal the signature exactly, not merely contain it"
);
}
#[test]
fn a_second_run_does_not_rewrite_an_existing_valid_tag() {
let root = tempfile::tempdir().expect("temp dir");
let cache_dir = root.path().join("cache");
std::fs::create_dir_all(&cache_dir).expect("create cache dir");
let tag_path = cache_dir.join(TAG_FILE_NAME);
let custom_body = format!("{SIGNATURE_LINE}\n# a human-edited comment that must survive\n");
std::fs::write(&tag_path, &custom_body).expect("plant an existing valid tag");
let mtime_before = std::fs::metadata(&tag_path)
.expect("stat before")
.modified()
.expect("mtime");
std::thread::sleep(std::time::Duration::from_millis(1100));
ensure_cache_dir(&cache_dir).expect("second call must succeed");
let content_after = std::fs::read_to_string(&tag_path).expect("read tag after second run");
let mtime_after = std::fs::metadata(&tag_path)
.expect("stat after")
.modified()
.expect("mtime");
assert_eq!(
content_after, custom_body,
"an existing valid tag's content, including a user-added comment, must survive byte-for-byte"
);
assert_eq!(
mtime_before, mtime_after,
"an existing valid tag must not be rewritten -- its mtime must not move"
);
}
#[test]
fn an_invalid_existing_tag_file_is_left_untouched_rather_than_clobbered() {
let root = tempfile::tempdir().expect("temp dir");
let cache_dir = root.path().join("cache");
std::fs::create_dir_all(&cache_dir).expect("create cache dir");
let tag_path = cache_dir.join(TAG_FILE_NAME);
let foreign_content = "not a cache directory tag\njust some other file\n";
std::fs::write(&tag_path, foreign_content).expect("plant a foreign, non-signature file");
ensure_cache_dir(&cache_dir).expect("must not error even when the tag path is occupied");
let content_after = std::fs::read_to_string(&tag_path).expect("read after ensure_cache_dir");
assert_eq!(
content_after, foreign_content,
"a file at CACHEDIR.TAG whose first line is not the signature must never be overwritten"
);
}
#[test]
fn a_directory_that_cannot_be_tagged_is_still_created_and_usable() {
let root = tempfile::tempdir().expect("temp dir");
let cache_dir = root.path().join("cache");
std::fs::create_dir_all(&cache_dir).expect("create cache dir");
std::fs::create_dir_all(cache_dir.join(TAG_FILE_NAME)).expect("occupy tag path with a directory");
let result = ensure_cache_dir(&cache_dir);
assert!(
result.is_ok(),
"a cache directory that cannot be tagged must still succeed, not fail the caller: {result:?}"
);
assert!(
cache_dir.is_dir(),
"the cache directory itself must remain usable regardless of the tag outcome"
);
}
#[test]
fn has_valid_signature_rejects_the_signature_on_a_later_line() {
let content = format!("not the first line\n{SIGNATURE_LINE}\n");
assert!(!has_valid_signature(content.as_bytes()));
}
#[test]
fn has_valid_signature_accepts_exactly_the_signature_with_trailing_content() {
let content = format!("{SIGNATURE_LINE}\n# trailing comment\n");
assert!(has_valid_signature(content.as_bytes()));
}
}