1use sha2::{Digest, Sha256};
7use std::fs;
8use std::io;
9use std::path::Path;
10
11use crate::error::Result;
12
13pub const HASH_LEN: usize = 32;
15
16pub const HASH_HEX_LEN: usize = 64;
18
19pub fn hash_bytes(data: &[u8]) -> String {
21 let mut hasher = Sha256::new();
22 hasher.update(data);
23 format!("{:x}", hasher.finalize())
24}
25
26pub fn hash_bytes_raw(data: &[u8]) -> [u8; HASH_LEN] {
28 let mut hasher = Sha256::new();
29 hasher.update(data);
30 let mut out = [0u8; HASH_LEN];
31 out.copy_from_slice(&hasher.finalize());
32 out
33}
34
35pub fn hash_file(path: &Path) -> Result<String> {
40 let mut file = fs::File::open(path)?;
41 let mut hasher = Sha256::new();
42 io::copy(&mut file, &mut hasher)?;
43 Ok(format!("{:x}", hasher.finalize()))
44}
45
46pub fn hash_reader(reader: &mut impl io::Read) -> Result<String> {
51 let mut hasher = Sha256::new();
52 io::copy(reader, &mut hasher)?;
53 Ok(format!("{:x}", hasher.finalize()))
54}
55
56pub fn verify_hash(data: &[u8], expected_hex: &str) -> bool {
60 hash_bytes(data) == expected_hex
61}
62
63pub fn verify_file_hash(path: &Path, expected_hex: &str) -> Result<bool> {
67 let actual = hash_file(path)?;
68 Ok(actual == expected_hex)
69}
70
71pub struct StreamingHasher {
76 hasher: Sha256,
77}
78
79impl StreamingHasher {
80 pub fn new() -> Self {
82 Self {
83 hasher: Sha256::new(),
84 }
85 }
86
87 pub fn update(&mut self, data: &[u8]) {
89 self.hasher.update(data);
90 }
91
92 pub fn finalize(self) -> String {
94 format!("{:x}", self.hasher.finalize())
95 }
96
97 pub fn finalize_raw(self) -> [u8; HASH_LEN] {
99 let mut out = [0u8; HASH_LEN];
100 out.copy_from_slice(&self.hasher.finalize());
101 out
102 }
103}
104
105impl Default for StreamingHasher {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use tempfile::TempDir;
115
116 #[test]
117 fn test_hash_bytes_known_answer() {
118 assert_eq!(
119 hash_bytes(b"hello world"),
120 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
121 );
122 }
123
124 #[test]
125 fn test_hash_bytes_empty() {
126 assert_eq!(
127 hash_bytes(b""),
128 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
129 );
130 }
131
132 #[test]
133 fn test_hash_bytes_raw_length() {
134 let raw = hash_bytes_raw(b"test");
135 assert_eq!(raw.len(), HASH_LEN);
136 }
137
138 #[test]
139 fn test_hash_file() -> Result<()> {
140 let tmp = TempDir::new()?;
141 let path = tmp.path().join("data.bin");
142 fs::write(&path, b"hello world")?;
143
144 let hash = hash_file(&path)?;
145 assert_eq!(
146 hash,
147 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
148 );
149 Ok(())
150 }
151
152 #[test]
153 fn test_hash_file_large() -> Result<()> {
154 let tmp = TempDir::new()?;
155 let path = tmp.path().join("large.bin");
156 let data = vec![0x42u8; 10 * 1024 * 1024];
158 fs::write(&path, &data)?;
159
160 let hash = hash_file(&path)?;
161 assert_eq!(hash.len(), HASH_HEX_LEN);
162 assert_eq!(hash, hash_bytes(&data));
163 Ok(())
164 }
165
166 #[test]
167 fn test_verify_hash_match() {
168 let data = b"verify me";
169 let hash = hash_bytes(data);
170 assert!(verify_hash(data, &hash));
171 }
172
173 #[test]
174 fn test_verify_hash_mismatch() {
175 assert!(!verify_hash(b"real data", "deadbeef"));
176 }
177
178 #[test]
179 fn test_verify_file_hash() -> Result<()> {
180 let tmp = TempDir::new()?;
181 let path = tmp.path().join("verify.bin");
182 fs::write(&path, b"trust but verify")?;
183
184 let expected = hash_bytes(b"trust but verify");
185 assert!(verify_file_hash(&path, &expected)?);
186 Ok(())
187 }
188
189 #[test]
190 fn test_streaming_hasher() {
191 let mut hasher = StreamingHasher::new();
192 hasher.update(b"hello ");
193 hasher.update(b"world");
194 let hash = hasher.finalize();
195
196 assert_eq!(
197 hash,
198 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
199 );
200 }
201
202 #[test]
203 fn test_streaming_hasher_matches_oneshot() {
204 let chunks: Vec<&[u8]> = vec![b"chunk1", b"chunk2", b"chunk3", b"chunk4"];
205 let combined: Vec<u8> = chunks.iter().flat_map(|c| c.iter()).copied().collect();
206
207 let mut hasher = StreamingHasher::new();
208 for chunk in &chunks {
209 hasher.update(chunk);
210 }
211 assert_eq!(hasher.finalize(), hash_bytes(&combined));
212 }
213
214 #[test]
215 fn test_hash_reader() -> Result<()> {
216 let data = b"streaming reader test";
217 let hash = hash_reader(&mut &data[..])?;
218 assert_eq!(hash, hash_bytes(data));
219 Ok(())
220 }
221}