use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use blake3::Hasher;
use walkdir::WalkDir;
use crate::error::{AugentError, Result};
pub const HASH_PREFIX: &str = "blake3:";
pub fn hash_file(path: &Path) -> Result<String> {
let file = File::open(path).map_err(|e| AugentError::FileReadFailed {
path: path.display().to_string(),
reason: e.to_string(),
})?;
let mut reader = BufReader::new(file);
let mut hasher = Hasher::new();
let mut buffer = [0u8; 8192];
loop {
let bytes_read = reader
.read(&mut buffer)
.map_err(|e| AugentError::FileReadFailed {
path: path.display().to_string(),
reason: e.to_string(),
})?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(format!("{}{}", HASH_PREFIX, hasher.finalize().to_hex()))
}
pub fn hash_directory(path: &Path) -> Result<String> {
if !path.is_dir() {
return Err(AugentError::FileNotFound {
path: path.display().to_string(),
});
}
let mut hasher = Hasher::new();
let mut files: Vec<_> = WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.filter(|e| {
let name = e.file_name().to_string_lossy();
name != "augent.lock" && name != "augent.index.yaml"
})
.collect();
files.sort_by_key(|e| e.path().to_path_buf());
for entry in files {
let file_path = entry.path();
let relative_path = file_path
.strip_prefix(path)
.unwrap_or(file_path)
.to_string_lossy();
hasher.update(relative_path.as_bytes());
hasher.update(b"\0");
let file = File::open(file_path).map_err(|e| AugentError::FileReadFailed {
path: file_path.display().to_string(),
reason: e.to_string(),
})?;
let mut reader = BufReader::new(file);
let mut buffer = [0u8; 8192];
loop {
let bytes_read = reader
.read(&mut buffer)
.map_err(|e| AugentError::FileReadFailed {
path: file_path.display().to_string(),
reason: e.to_string(),
})?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
hasher.update(b"\0"); }
Ok(format!("{}{}", HASH_PREFIX, hasher.finalize().to_hex()))
}
pub fn verify_hash(expected: &str, actual: &str) -> bool {
let normalize = |h: &str| {
if h.starts_with(HASH_PREFIX) {
h.to_string()
} else {
format!("{}{}", HASH_PREFIX, h)
}
};
normalize(expected) == normalize(actual)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_hash_file() {
let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
let file_path = temp.path().join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
let hash = hash_file(&file_path).unwrap();
assert!(hash.starts_with(HASH_PREFIX));
}
#[test]
fn test_hash_file_not_found() {
let result = hash_file(Path::new("/nonexistent/file.txt"));
assert!(result.is_err());
}
#[test]
fn test_hash_directory() {
let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
std::fs::write(temp.path().join("file1.txt"), "content1").unwrap();
std::fs::create_dir(temp.path().join("subdir")).unwrap();
std::fs::write(temp.path().join("subdir/file2.txt"), "content2").unwrap();
let hash = hash_directory(temp.path()).unwrap();
assert!(hash.starts_with(HASH_PREFIX));
}
#[test]
fn test_hash_directory_deterministic() {
let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
std::fs::write(temp.path().join("a.txt"), "aaa").unwrap();
std::fs::write(temp.path().join("b.txt"), "bbb").unwrap();
let hash1 = hash_directory(temp.path()).unwrap();
let hash2 = hash_directory(temp.path()).unwrap();
assert_eq!(hash1, hash2);
}
#[test]
fn test_hash_directory_excludes_lockfile() {
let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
std::fs::write(temp.path().join("file.txt"), "content").unwrap();
let hash1 = hash_directory(temp.path()).unwrap();
std::fs::write(temp.path().join("augent.lock"), "lock content").unwrap();
let hash2 = hash_directory(temp.path()).unwrap();
assert_eq!(hash1, hash2);
}
#[test]
fn test_verify_hash() {
let hash1 = format!("{}abc123", HASH_PREFIX);
let hash2 = hash1.clone();
assert!(verify_hash(&hash1, &hash2));
let hash_with_prefix = format!("{}abc123", HASH_PREFIX);
let hash_without_prefix = "abc123";
assert!(verify_hash(&hash_with_prefix, hash_without_prefix));
let hash3 = format!("{}def456", HASH_PREFIX);
assert!(!verify_hash(&hash1, &hash3));
}
}