1use base64::{engine::general_purpose::STANDARD, Engine as _};
2use sha2::{Digest, Sha512};
3
4use crate::errors::{DnxError, Result};
5
6pub fn compute_sha512(data: &[u8]) -> String {
27 let mut hasher = Sha512::new();
28 hasher.update(data);
29 let hash = hasher.finalize();
30 let encoded = STANDARD.encode(hash);
31 format!("sha512-{}", encoded)
32}
33
34pub fn verify_integrity(data: &[u8], expected: &str) -> Result<bool> {
63 let prefix = "sha512-";
65 if !expected.starts_with(prefix) {
66 return Err(DnxError::InvalidIntegrity(
67 "Integrity hash must start with 'sha512-'".to_string(),
68 ));
69 }
70
71 let base64_hash = &expected[prefix.len()..];
72
73 let expected_hash = STANDARD
75 .decode(base64_hash)
76 .map_err(|e| DnxError::InvalidIntegrity(format!("Invalid base64 encoding: {}", e)))?;
77
78 let mut hasher = Sha512::new();
80 hasher.update(data);
81 let actual_hash = hasher.finalize();
82
83 Ok(expected_hash.as_slice() == actual_hash.as_slice())
85}
86
87pub fn integrity_to_path_safe(integrity: &str) -> String {
111 integrity.replace('/', "_").replace('+', "-")
112}
113
114pub struct StreamingHash {
131 hasher: Sha512,
132}
133
134impl StreamingHash {
135 pub fn new() -> Self {
137 Self {
138 hasher: Sha512::new(),
139 }
140 }
141
142 pub fn update(&mut self, data: &[u8]) {
148 self.hasher.update(data);
149 }
150
151 pub fn finalize(self) -> String {
159 let hash = self.hasher.finalize();
160 let encoded = STANDARD.encode(hash);
161 format!("sha512-{}", encoded)
162 }
163}
164
165impl Default for StreamingHash {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_compute_sha512() {
177 let data = b"hello world";
178 let integrity = compute_sha512(data);
179 assert!(integrity.starts_with("sha512-"));
180 assert!(integrity.len() > 7);
181 }
182
183 #[test]
184 fn test_verify_integrity_valid() {
185 let data = b"hello world";
186 let integrity = compute_sha512(data);
187 assert!(verify_integrity(data, &integrity).unwrap());
188 }
189
190 #[test]
191 fn test_verify_integrity_invalid() {
192 let data = b"hello world";
193 let wrong_data = b"goodbye world";
194 let integrity = compute_sha512(data);
195 assert!(!verify_integrity(wrong_data, &integrity).unwrap());
196 }
197
198 #[test]
199 fn test_verify_integrity_invalid_prefix() {
200 let data = b"hello world";
201 let result = verify_integrity(data, "sha256-abc123");
202 assert!(result.is_err());
203 }
204
205 #[test]
206 fn test_verify_integrity_invalid_base64() {
207 let data = b"hello world";
208 let result = verify_integrity(data, "sha512-@@@invalid@@@");
209 assert!(result.is_err());
210 }
211
212 #[test]
213 fn test_integrity_to_path_safe() {
214 let integrity = "sha512-abc+def/ghi==";
215 let safe = integrity_to_path_safe(integrity);
216 assert_eq!(safe, "sha512-abc-def_ghi==");
217 }
218
219 #[test]
220 fn test_integrity_to_path_safe_no_special_chars() {
221 let integrity = "sha512-abcdefghi==";
222 let safe = integrity_to_path_safe(integrity);
223 assert_eq!(safe, "sha512-abcdefghi==");
224 }
225
226 #[test]
227 fn test_streaming_hash() {
228 let mut hasher = StreamingHash::new();
229 hasher.update(b"hello ");
230 hasher.update(b"world");
231 let integrity = hasher.finalize();
232
233 let direct = compute_sha512(b"hello world");
234 assert_eq!(integrity, direct);
235 }
236
237 #[test]
238 fn test_streaming_hash_empty() {
239 let hasher = StreamingHash::new();
240 let integrity = hasher.finalize();
241 let direct = compute_sha512(b"");
242 assert_eq!(integrity, direct);
243 }
244
245 #[test]
246 fn test_streaming_hash_single_update() {
247 let mut hasher = StreamingHash::new();
248 hasher.update(b"test data");
249 let integrity = hasher.finalize();
250 let direct = compute_sha512(b"test data");
251 assert_eq!(integrity, direct);
252 }
253}