use std::fs::File;
use std::path::Path;
use serde::Serialize;
use crate::error::Result;
pub const ALGORITHM: &str = "blake3";
pub fn hash_file(path: &Path) -> Result<String> {
let file = File::open(path)?;
let mut hasher = blake3::Hasher::new();
hasher.update_reader(file)?;
Ok(hasher.finalize().to_hex().as_str().to_owned())
}
#[derive(Serialize)]
pub struct FileHash {
pub path: String,
pub hash: String,
}
pub fn hash_each(base: &Path, rel_paths: &[String]) -> Result<Vec<FileHash>> {
rel_paths
.iter()
.map(|rel| {
Ok(FileHash {
path: rel.clone(),
hash: hash_file(&base.join(rel))?,
})
})
.collect()
}
#[must_use]
pub fn digest_hashes(files: &[FileHash]) -> String {
let mut hasher = blake3::Hasher::new();
for file in files {
hasher.update(file.path.as_bytes());
hasher.update(b"\0");
hasher.update(file.hash.as_bytes());
hasher.update(b"\n");
}
hasher.finalize().to_hex().as_str().to_owned()
}
pub fn digest_files(base: &Path, rel_paths: &[String]) -> Result<String> {
Ok(digest_hashes(&hash_each(base, rel_paths)?))
}
#[cfg(test)]
mod tests {
use super::{digest_files, hash_file};
#[test]
fn hashes_are_stable_and_content_dependent() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("a.txt");
std::fs::write(&path, b"hello").expect("write");
let first = hash_file(&path).expect("hash");
assert_eq!(first.len(), 64, "blake3 hex is 64 chars");
assert_eq!(first, hash_file(&path).expect("hash"), "deterministic");
std::fs::write(&path, b"world").expect("rewrite");
assert_ne!(first, hash_file(&path).expect("hash"), "content matters");
}
#[test]
fn digest_reflects_content_and_membership() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("a.txt"), b"one").expect("write");
std::fs::write(dir.path().join("b.txt"), b"two").expect("write");
let both = ["a.txt".to_owned(), "b.txt".to_owned()];
let base = digest_files(dir.path(), &both).expect("digest");
std::fs::write(dir.path().join("a.txt"), b"edited").expect("rewrite");
assert_ne!(
base,
digest_files(dir.path(), &both).expect("digest"),
"edit shifts digest"
);
let one = ["b.txt".to_owned()];
assert_ne!(
base,
digest_files(dir.path(), &one).expect("digest"),
"membership shifts digest"
);
}
}