1use sha2::{Digest, Sha256};
4use std::io;
5use std::path::Path;
6
7pub fn content_hash(content: &str) -> String {
9 bytes_hash(content.as_bytes())
10}
11
12pub fn short_content_hash(content: &str) -> String {
14 let hash = content_hash(content);
15 hash[..hash.len().min(12)].to_string()
16}
17
18pub fn bytes_hash(bytes: &[u8]) -> String {
20 let mut hasher = Sha256::new();
21 hasher.update(bytes);
22 hex::encode(hasher.finalize())
23}
24
25pub fn path_hash(path: &Path) -> io::Result<String> {
27 let canonical = path.canonicalize()?;
28 Ok(path_string_hash(&canonical.to_string_lossy()))
29}
30
31pub fn path_string_hash(path: &str) -> String {
33 content_hash(path)
34}
35
36pub fn document_id_for_path(path: &Path) -> String {
42 path_hash(path).unwrap_or_else(|_| path.display().to_string())
43}
44
45#[cfg(test)]
46mod tests {
47 use super::{
48 bytes_hash, content_hash, document_id_for_path, path_hash, path_string_hash,
49 short_content_hash,
50 };
51 use std::path::Path;
52 use tempfile::TempDir;
53
54 #[test]
55 fn content_hash_matches_sha256_hex() {
56 assert_eq!(
57 content_hash("hello"),
58 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
59 );
60 assert_eq!(
61 content_hash(""),
62 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
63 );
64 }
65
66 #[test]
67 fn bytes_hash_matches_text_hash_for_utf8() {
68 assert_eq!(bytes_hash(b"hello"), content_hash("hello"));
69 }
70
71 #[test]
72 fn short_content_hash_uses_stable_sha256_prefix() {
73 assert_eq!(short_content_hash("hello"), &content_hash("hello")[..12]);
74 assert_eq!(short_content_hash(""), &content_hash("")[..12]);
75 }
76
77 #[test]
78 fn path_hash_uses_canonical_path_string() {
79 let dir = TempDir::new().unwrap();
80 let file = dir.path().join("doc.md");
81 std::fs::write(&file, "").unwrap();
82 let canonical = file.canonicalize().unwrap();
83
84 assert_eq!(
85 path_hash(&file).unwrap(),
86 path_string_hash(&canonical.to_string_lossy())
87 );
88 assert_eq!(document_id_for_path(&file), path_hash(&file).unwrap());
89 }
90
91 #[test]
92 fn document_id_for_path_preserves_missing_path_fallback() {
93 let missing = Path::new("definitely/missing.md");
94 assert_eq!(document_id_for_path(missing), missing.display().to_string());
95 }
96}