1use std::path::Path;
17
18use sha2::{Digest as Sha2Digest, Sha256, Sha512};
19use sha3::Sha3_512;
20
21#[doc(hidden)]
26pub use generic_array::typenum::Unsigned as _GenericArrayUnsigned;
27
28use crate::error::Result;
29
30#[must_use]
32pub fn sha256_hex(data: &[u8]) -> String {
33 let mut hasher = Sha256::new();
34 hasher.update(data);
35 let result = hasher.finalize();
36 hex_encode(&result)
37}
38
39#[must_use]
41pub fn sha512_hex(data: &[u8]) -> String {
42 let mut hasher = Sha512::new();
43 hasher.update(data);
44 let result = hasher.finalize();
45 hex_encode(&result)
46}
47
48#[must_use]
50pub fn sha3_512_hex(data: &[u8]) -> String {
51 let mut hasher = Sha3_512::new();
52 hasher.update(data);
53 let result = hasher.finalize();
54 hex_encode(&result)
55}
56
57#[must_use]
62pub fn generate_hashes(data: &[u8]) -> (String, String) {
63 (sha256_hex(data), sha3_512_hex(data))
64}
65
66#[must_use]
68pub fn generate_all_hashes(data: &[u8]) -> (String, String, String) {
69 (sha256_hex(data), sha512_hex(data), sha3_512_hex(data))
70}
71
72pub fn write_sidecar_files(
82 json_path: &Path,
83 data: &[u8],
84 write_sha256: bool,
85 write_sha512: bool,
86 write_sha3_512: bool,
87) -> Result<()> {
88 let filename = json_path
89 .file_name()
90 .map(|f| f.to_string_lossy().to_string())
91 .unwrap_or_default();
92
93 if write_sha256 {
94 let hash = sha256_hex(data);
95 let sidecar_path = json_path.with_extension("json.sha-256");
96 let content = format!("{hash} {filename}\n");
97 std::fs::write(&sidecar_path, content)?;
98 }
99
100 if write_sha512 {
101 let hash = sha512_hex(data);
102 let sidecar_path = json_path.with_extension("json.sha-512");
103 let content = format!("{hash} {filename}\n");
104 std::fs::write(&sidecar_path, content)?;
105 }
106
107 if write_sha3_512 {
108 let hash = sha3_512_hex(data);
109 let sidecar_path = json_path.with_extension("json.sha3-512");
110 let content = format!("{hash} {filename}\n");
111 std::fs::write(&sidecar_path, content)?;
112 }
113
114 Ok(())
115}
116
117#[allow(clippy::type_complexity)]
133pub fn write_sidecar_files_for(
134 file_path: &Path,
135 data: &[u8],
136 write_sha256: bool,
137 write_sha512: bool,
138 write_sha3_512: bool,
139) -> Result<(
140 Option<std::path::PathBuf>,
141 Option<std::path::PathBuf>,
142 Option<std::path::PathBuf>,
143)> {
144 let filename = file_path
145 .file_name()
146 .map(|f| f.to_string_lossy().to_string())
147 .unwrap_or_default();
148
149 let mut sha256_path = None;
150 let mut sha512_path = None;
151 let mut sha3_path = None;
152
153 if write_sha256 {
154 let hash = sha256_hex(data);
155 let mut sidecar = file_path.as_os_str().to_owned();
156 sidecar.push(".sha-256");
157 let sidecar_path = std::path::PathBuf::from(sidecar);
158 let content = format!("{hash} {filename}\n");
159 std::fs::write(&sidecar_path, content)?;
160 sha256_path = Some(sidecar_path);
161 }
162
163 if write_sha512 {
164 let hash = sha512_hex(data);
165 let mut sidecar = file_path.as_os_str().to_owned();
166 sidecar.push(".sha-512");
167 let sidecar_path = std::path::PathBuf::from(sidecar);
168 let content = format!("{hash} {filename}\n");
169 std::fs::write(&sidecar_path, content)?;
170 sha512_path = Some(sidecar_path);
171 }
172
173 if write_sha3_512 {
174 let hash = sha3_512_hex(data);
175 let mut sidecar = file_path.as_os_str().to_owned();
176 sidecar.push(".sha3-512");
177 let sidecar_path = std::path::PathBuf::from(sidecar);
178 let content = format!("{hash} {filename}\n");
179 std::fs::write(&sidecar_path, content)?;
180 sha3_path = Some(sidecar_path);
181 }
182
183 Ok((sha256_path, sha512_path, sha3_path))
184}
185
186fn hex_encode(bytes: &[u8]) -> String {
188 use std::fmt::Write as _;
189 let mut hex = String::with_capacity(bytes.len() * 2);
190 for b in bytes {
191 let _ = write!(hex, "{b:02x}");
192 }
193 hex
194}
195
196#[cfg(test)]
197#[allow(clippy::case_sensitive_file_extension_comparisons)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_sha256_known_value() {
205 let hash = sha256_hex(b"");
207 assert_eq!(
208 hash,
209 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
210 );
211 }
212
213 #[test]
214 fn test_sha256_hello() {
215 let hash = sha256_hex(b"hello");
216 assert_eq!(
217 hash,
218 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
219 );
220 }
221
222 #[test]
223 fn test_sha512_known_value() {
224 let hash = sha512_hex(b"");
226 assert_eq!(
227 hash,
228 "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\
229 47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
230 );
231 }
232
233 #[test]
234 fn test_sha512_hello() {
235 let hash = sha512_hex(b"hello");
236 assert_eq!(
237 hash,
238 "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca7\
239 2323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
240 );
241 }
242
243 #[test]
244 fn test_sha3_512_known_value() {
245 let hash = sha3_512_hex(b"");
247 assert_eq!(
248 hash,
249 "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615\
250 b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
251 );
252 }
253
254 #[test]
255 fn test_generate_hashes_legacy() {
256 let (sha256, sha3) = generate_hashes(b"test data");
257 assert_eq!(sha256.len(), 64); assert_eq!(sha3.len(), 128); }
260
261 #[test]
262 fn test_generate_all_hashes_triplet() {
263 let (sha256, sha512, sha3) = generate_all_hashes(b"test data");
264 assert_eq!(sha256.len(), 64);
265 assert_eq!(sha512.len(), 128);
266 assert_eq!(sha3.len(), 128);
267 assert_ne!(sha256, sha512);
269 assert_ne!(sha512, sha3);
270 assert_ne!(sha256, sha3);
271 }
272
273 #[test]
274 fn test_deterministic() {
275 let data = b"CSAF document content";
276 let (h1_256, h1_512, h1_3) = generate_all_hashes(data);
277 let (h2_256, h2_512, h2_3) = generate_all_hashes(data);
278 assert_eq!(h1_256, h2_256);
279 assert_eq!(h1_512, h2_512);
280 assert_eq!(h1_3, h2_3);
281 }
282
283 #[test]
284 fn test_write_sidecar_files() {
285 let dir = tempfile::tempdir().expect("tmpdir failed");
286 let json_path = dir.path().join("test.json");
287 let data = b"{\"test\": true}";
288 std::fs::write(&json_path, data).expect("write failed");
289
290 write_sidecar_files(&json_path, data, true, true, true).expect("sidecar write failed");
291
292 let sha256_path = dir.path().join("test.json.sha-256");
293 let sha512_path = dir.path().join("test.json.sha-512");
294 let sha3_path = dir.path().join("test.json.sha3-512");
295
296 assert!(sha256_path.exists());
297 assert!(sha512_path.exists());
298 assert!(sha3_path.exists());
299
300 let sha256_content = std::fs::read_to_string(&sha256_path).expect("read failed");
301 assert!(sha256_content.contains("test.json"));
302 assert!(sha256_content.contains(" ")); assert!(!dir.path().join("test.json.sha256").exists());
306 assert!(!dir.path().join("test.json.sha512").exists());
307 }
308
309 #[test]
310 fn test_write_only_sha256() {
311 let dir = tempfile::tempdir().expect("tmpdir failed");
312 let json_path = dir.path().join("test.json");
313 let data = b"{}";
314 std::fs::write(&json_path, data).expect("write failed");
315
316 write_sidecar_files(&json_path, data, true, false, false).expect("sidecar write failed");
317
318 assert!(dir.path().join("test.json.sha-256").exists());
319 assert!(!dir.path().join("test.json.sha-512").exists());
320 assert!(!dir.path().join("test.json.sha3-512").exists());
321 }
322
323 #[test]
324 fn test_write_only_sha512() {
325 let dir = tempfile::tempdir().expect("tmpdir failed");
326 let json_path = dir.path().join("test.json");
327 let data = b"{}";
328 std::fs::write(&json_path, data).expect("write failed");
329
330 write_sidecar_files(&json_path, data, false, true, false).expect("sidecar write failed");
331
332 assert!(!dir.path().join("test.json.sha-256").exists());
333 assert!(dir.path().join("test.json.sha-512").exists());
334 assert!(!dir.path().join("test.json.sha3-512").exists());
335 }
336
337 #[test]
338 fn test_write_sidecar_files_for_redb() {
339 let dir = tempfile::tempdir().expect("tmpdir failed");
340 let redb_path = dir.path().join("csaf.redb");
341 let data = b"dummy-redb-bytes";
342 std::fs::write(&redb_path, data).expect("write failed");
343
344 let (s256, s512, s3) = write_sidecar_files_for(&redb_path, data, true, true, true)
345 .expect("sidecar write failed");
346
347 let s256 = s256.expect("sha256 path");
348 let s512 = s512.expect("sha512 path");
349 let s3 = s3.expect("sha3 path");
350 assert_eq!(s256.file_name().unwrap(), "csaf.redb.sha-256");
351 assert_eq!(s512.file_name().unwrap(), "csaf.redb.sha-512");
352 assert_eq!(s3.file_name().unwrap(), "csaf.redb.sha3-512");
353 assert!(s256.exists());
354 assert!(s512.exists());
355 assert!(s3.exists());
356
357 let sha_content = std::fs::read_to_string(&s256).expect("read");
358 assert!(sha_content.contains("csaf.redb"));
359 assert!(sha_content.contains(" "));
360 assert!(sha_content.contains(&sha256_hex(data)));
361
362 let sha512_content = std::fs::read_to_string(&s512).expect("read");
363 assert!(sha512_content.contains(&sha512_hex(data)));
364 }
365
366 #[test]
367 fn test_write_sidecar_files_for_skip_sha3() {
368 let dir = tempfile::tempdir().expect("tmpdir failed");
369 let path = dir.path().join("csaf.sqlite");
370 let data = b"dummy-sqlite";
371 std::fs::write(&path, data).expect("write failed");
372
373 let (s256, s512, s3) =
374 write_sidecar_files_for(&path, data, true, true, false).expect("sidecar write");
375 assert!(s256.is_some());
376 assert!(s512.is_some());
377 assert!(s3.is_none());
378 assert!(!dir.path().join("csaf.sqlite.sha3-512").exists());
379 }
380
381 #[test]
382 fn test_write_sidecar_files_for_skip_all_but_sha3() {
383 let dir = tempfile::tempdir().expect("tmpdir failed");
384 let path = dir.path().join("evidence.bin");
385 let data = b"x";
386 std::fs::write(&path, data).expect("write failed");
387
388 let (s256, s512, s3) =
389 write_sidecar_files_for(&path, data, false, false, true).expect("sidecar write");
390 assert!(s256.is_none());
391 assert!(s512.is_none());
392 assert!(s3.is_some());
393 assert!(dir.path().join("evidence.bin.sha3-512").exists());
394 }
395
396 #[test]
397 fn test_extension_uses_hyphenated_form_only() {
398 let dir = tempfile::tempdir().expect("tmpdir failed");
401 let path = dir.path().join("payload.json");
402 std::fs::write(&path, b"payload").expect("write failed");
403
404 write_sidecar_files(&path, b"payload", true, true, true).expect("sidecar write");
405
406 for entry in std::fs::read_dir(dir.path()).expect("readdir") {
407 let name = entry.unwrap().file_name().to_string_lossy().to_string();
408 assert!(
410 name.ends_with(".json")
411 || name.ends_with(".json.sha-256")
412 || name.ends_with(".json.sha-512")
413 || name.ends_with(".json.sha3-512"),
414 "unexpected sidecar extension: {name}"
415 );
416 assert!(
417 !(name.ends_with(".sha256") || name.ends_with(".sha512")),
418 "legacy unhyphenated form leaked: {name}"
419 );
420 }
421 }
422
423 #[test]
424 fn test_sidecar_matches_csaf_file() {
425 let json = include_str!("../../../test/csaf/2026/003/ndaal-sa-2026-003.json");
426 let (sha256, sha512, sha3) = generate_all_hashes(json.as_bytes());
427
428 assert_eq!(sha256.len(), 64);
430 assert_eq!(sha512.len(), 128);
431 assert_eq!(sha3.len(), 128);
432
433 let (sha256_again, sha512_again, sha3_again) = generate_all_hashes(json.as_bytes());
435 assert_eq!(sha256, sha256_again);
436 assert_eq!(sha512, sha512_again);
437 assert_eq!(sha3, sha3_again);
438 }
439}