use sha2::{Digest, Sha256};
use std::io;
use std::path::Path;
pub fn content_hash(content: &str) -> String {
bytes_hash(content.as_bytes())
}
pub fn short_content_hash(content: &str) -> String {
let hash = content_hash(content);
hash[..hash.len().min(12)].to_string()
}
pub fn bytes_hash(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
pub fn path_hash(path: &Path) -> io::Result<String> {
let canonical = path.canonicalize()?;
Ok(path_string_hash(&canonical.to_string_lossy()))
}
pub fn path_string_hash(path: &str) -> String {
content_hash(path)
}
pub fn document_id_for_path(path: &Path) -> String {
path_hash(path).unwrap_or_else(|_| path.display().to_string())
}
#[cfg(test)]
mod tests {
use super::{
bytes_hash, content_hash, document_id_for_path, path_hash, path_string_hash,
short_content_hash,
};
use std::path::Path;
use tempfile::TempDir;
#[test]
fn content_hash_matches_sha256_hex() {
assert_eq!(
content_hash("hello"),
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
);
assert_eq!(
content_hash(""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn bytes_hash_matches_text_hash_for_utf8() {
assert_eq!(bytes_hash(b"hello"), content_hash("hello"));
}
#[test]
fn short_content_hash_uses_stable_sha256_prefix() {
assert_eq!(short_content_hash("hello"), &content_hash("hello")[..12]);
assert_eq!(short_content_hash(""), &content_hash("")[..12]);
}
#[test]
fn path_hash_uses_canonical_path_string() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("doc.md");
std::fs::write(&file, "").unwrap();
let canonical = file.canonicalize().unwrap();
assert_eq!(
path_hash(&file).unwrap(),
path_string_hash(&canonical.to_string_lossy())
);
assert_eq!(document_id_for_path(&file), path_hash(&file).unwrap());
}
#[test]
fn document_id_for_path_preserves_missing_path_fallback() {
let missing = Path::new("definitely/missing.md");
assert_eq!(document_id_for_path(missing), missing.display().to_string());
}
}