ccsync_core/comparison/
hash.rs

1//! File hashing for content comparison using SHA-256
2
3use std::fs::File;
4use std::io::{BufReader, Read};
5use std::path::Path;
6
7use anyhow::Context;
8use sha2::{Digest, Sha256};
9
10use crate::error::Result;
11
12/// File hash result
13pub type FileHash = [u8; 32];
14
15/// File hasher
16pub struct FileHasher;
17
18impl Default for FileHasher {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl FileHasher {
25    /// Create a new file hasher
26    #[must_use]
27    pub const fn new() -> Self {
28        Self
29    }
30
31    /// Compute SHA-256 hash of a file by streaming its contents
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the file cannot be read.
36    pub fn hash(path: &Path) -> Result<FileHash> {
37        let file = File::open(path)
38            .with_context(|| format!("Failed to open file for hashing: {}", path.display()))?;
39
40        let mut reader = BufReader::new(file);
41        let mut hasher = Sha256::new();
42        let mut buffer = [0; 8192]; // 8KB buffer for streaming
43
44        loop {
45            let bytes_read = reader
46                .read(&mut buffer)
47                .with_context(|| format!("Failed to read file: {}", path.display()))?;
48
49            if bytes_read == 0 {
50                break;
51            }
52
53            hasher.update(&buffer[..bytes_read]);
54        }
55
56        Ok(hasher.finalize().into())
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::fs;
64    use tempfile::TempDir;
65
66    #[test]
67    fn test_hash_identical_files() {
68        let tmp = TempDir::new().unwrap();
69        let file1 = tmp.path().join("file1.txt");
70        let file2 = tmp.path().join("file2.txt");
71
72        fs::write(&file1, "same content").unwrap();
73        fs::write(&file2, "same content").unwrap();
74
75        let _hasher = FileHasher::new();
76        let hash1 = FileHasher::hash(&file1).unwrap();
77        let hash2 = FileHasher::hash(&file2).unwrap();
78
79        assert_eq!(hash1, hash2);
80    }
81
82    #[test]
83    fn test_hash_different_files() {
84        let tmp = TempDir::new().unwrap();
85        let file1 = tmp.path().join("file1.txt");
86        let file2 = tmp.path().join("file2.txt");
87
88        fs::write(&file1, "content 1").unwrap();
89        fs::write(&file2, "content 2").unwrap();
90
91        let _hasher = FileHasher::new();
92        let hash1 = FileHasher::hash(&file1).unwrap();
93        let hash2 = FileHasher::hash(&file2).unwrap();
94
95        assert_ne!(hash1, hash2);
96    }
97
98    #[test]
99    fn test_hash_large_file() {
100        let tmp = TempDir::new().unwrap();
101        let file = tmp.path().join("large.bin");
102
103        // Create a 1MB file
104        let content = vec![0u8; 1024 * 1024];
105        fs::write(&file, &content).unwrap();
106
107        let _hasher = FileHasher::new();
108        let hash = FileHasher::hash(&file);
109
110        assert!(hash.is_ok());
111    }
112
113    #[test]
114    fn test_hash_empty_file() {
115        let tmp = TempDir::new().unwrap();
116        let file = tmp.path().join("empty.txt");
117        fs::write(&file, "").unwrap();
118
119        let _hasher = FileHasher::new();
120        let hash = FileHasher::hash(&file);
121
122        assert!(hash.is_ok());
123    }
124}