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