1use std::path::Path;
7
8use sha2::{Digest as Sha2Digest, Sha256};
9use sha3::Sha3_512;
10
11use crate::error::Result;
12
13#[must_use]
15pub fn sha256_hex(data: &[u8]) -> String {
16 let mut hasher = Sha256::new();
17 hasher.update(data);
18 let result = hasher.finalize();
19 hex_encode(&result)
20}
21
22#[must_use]
24pub fn sha3_512_hex(data: &[u8]) -> String {
25 let mut hasher = Sha3_512::new();
26 hasher.update(data);
27 let result = hasher.finalize();
28 hex_encode(&result)
29}
30
31#[must_use]
33pub fn generate_hashes(data: &[u8]) -> (String, String) {
34 (sha256_hex(data), sha3_512_hex(data))
35}
36
37pub fn write_sidecar_files(
46 json_path: &Path,
47 data: &[u8],
48 write_sha256: bool,
49 write_sha3_512: bool,
50) -> Result<()> {
51 let filename = json_path
52 .file_name()
53 .map(|f| f.to_string_lossy().to_string())
54 .unwrap_or_default();
55
56 if write_sha256 {
57 let hash = sha256_hex(data);
58 let sidecar_path = json_path.with_extension("json.sha256");
59 let content = format!("{hash} {filename}\n");
60 std::fs::write(&sidecar_path, content)?;
61 }
62
63 if write_sha3_512 {
64 let hash = sha3_512_hex(data);
65 let sidecar_path = json_path.with_extension("json.sha3-512");
66 let content = format!("{hash} {filename}\n");
67 std::fs::write(&sidecar_path, content)?;
68 }
69
70 Ok(())
71}
72
73pub fn write_sidecar_files_for(
88 file_path: &Path,
89 data: &[u8],
90 write_sha256: bool,
91 write_sha3_512: bool,
92) -> Result<(Option<std::path::PathBuf>, Option<std::path::PathBuf>)> {
93 let filename = file_path
94 .file_name()
95 .map(|f| f.to_string_lossy().to_string())
96 .unwrap_or_default();
97
98 let mut sha256_path = None;
99 let mut sha3_path = None;
100
101 if write_sha256 {
102 let hash = sha256_hex(data);
103 let mut sidecar = file_path.as_os_str().to_owned();
104 sidecar.push(".sha256");
105 let sidecar_path = std::path::PathBuf::from(sidecar);
106 let content = format!("{hash} {filename}\n");
107 std::fs::write(&sidecar_path, content)?;
108 sha256_path = Some(sidecar_path);
109 }
110
111 if write_sha3_512 {
112 let hash = sha3_512_hex(data);
113 let mut sidecar = file_path.as_os_str().to_owned();
114 sidecar.push(".sha3-512");
115 let sidecar_path = std::path::PathBuf::from(sidecar);
116 let content = format!("{hash} {filename}\n");
117 std::fs::write(&sidecar_path, content)?;
118 sha3_path = Some(sidecar_path);
119 }
120
121 Ok((sha256_path, sha3_path))
122}
123
124fn hex_encode(bytes: &[u8]) -> String {
126 use std::fmt::Write as _;
127 let mut hex = String::with_capacity(bytes.len() * 2);
128 for b in bytes {
129 let _ = write!(hex, "{b:02x}");
130 }
131 hex
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_sha256_known_value() {
140 let hash = sha256_hex(b"");
142 assert_eq!(
143 hash,
144 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
145 );
146 }
147
148 #[test]
149 fn test_sha256_hello() {
150 let hash = sha256_hex(b"hello");
151 assert_eq!(
152 hash,
153 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
154 );
155 }
156
157 #[test]
158 fn test_sha3_512_known_value() {
159 let hash = sha3_512_hex(b"");
161 assert_eq!(
162 hash,
163 "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615\
164 b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
165 );
166 }
167
168 #[test]
169 fn test_generate_hashes() {
170 let (sha256, sha3) = generate_hashes(b"test data");
171 assert_eq!(sha256.len(), 64); assert_eq!(sha3.len(), 128); }
174
175 #[test]
176 fn test_deterministic() {
177 let data = b"CSAF document content";
178 let (h1_256, h1_512) = generate_hashes(data);
179 let (h2_256, h2_512) = generate_hashes(data);
180 assert_eq!(h1_256, h2_256);
181 assert_eq!(h1_512, h2_512);
182 }
183
184 #[test]
185 fn test_write_sidecar_files() {
186 let dir = tempfile::tempdir().expect("tmpdir failed");
187 let json_path = dir.path().join("test.json");
188 let data = b"{\"test\": true}";
189 std::fs::write(&json_path, data).expect("write failed");
190
191 write_sidecar_files(&json_path, data, true, true).expect("sidecar write failed");
192
193 let sha256_path = dir.path().join("test.json.sha256");
194 let sha3_path = dir.path().join("test.json.sha3-512");
195
196 assert!(sha256_path.exists());
197 assert!(sha3_path.exists());
198
199 let sha256_content = std::fs::read_to_string(&sha256_path).expect("read failed");
200 assert!(sha256_content.contains("test.json"));
201 assert!(sha256_content.contains(" ")); }
203
204 #[test]
205 fn test_write_only_sha256() {
206 let dir = tempfile::tempdir().expect("tmpdir failed");
207 let json_path = dir.path().join("test.json");
208 let data = b"{}";
209 std::fs::write(&json_path, data).expect("write failed");
210
211 write_sidecar_files(&json_path, data, true, false).expect("sidecar write failed");
212
213 assert!(dir.path().join("test.json.sha256").exists());
214 assert!(!dir.path().join("test.json.sha3-512").exists());
215 }
216
217 #[test]
218 fn test_write_sidecar_files_for_redb() {
219 let dir = tempfile::tempdir().expect("tmpdir failed");
220 let redb_path = dir.path().join("csaf.redb");
221 let data = b"dummy-redb-bytes";
222 std::fs::write(&redb_path, data).expect("write failed");
223
224 let (s256, s3) =
225 write_sidecar_files_for(&redb_path, data, true, true).expect("sidecar write failed");
226
227 let s256 = s256.expect("sha256 path");
228 let s3 = s3.expect("sha3 path");
229 assert_eq!(s256.file_name().unwrap(), "csaf.redb.sha256");
230 assert_eq!(s3.file_name().unwrap(), "csaf.redb.sha3-512");
231 assert!(s256.exists());
232 assert!(s3.exists());
233
234 let sha_content = std::fs::read_to_string(&s256).expect("read");
235 assert!(sha_content.contains("csaf.redb"));
236 assert!(sha_content.contains(" "));
237 assert!(sha_content.contains(&sha256_hex(data)));
238 }
239
240 #[test]
241 fn test_write_sidecar_files_for_skip_one() {
242 let dir = tempfile::tempdir().expect("tmpdir failed");
243 let path = dir.path().join("csaf.sqlite");
244 let data = b"dummy-sqlite";
245 std::fs::write(&path, data).expect("write failed");
246
247 let (s256, s3) = write_sidecar_files_for(&path, data, true, false).expect("sidecar write");
248 assert!(s256.is_some());
249 assert!(s3.is_none());
250 assert!(!dir.path().join("csaf.sqlite.sha3-512").exists());
251 }
252
253 #[test]
254 fn test_sidecar_matches_csaf_file() {
255 let json = include_str!("../../../test/csaf/2026/003/ndaal-sa-2026-003.json");
256 let (sha256, sha3) = generate_hashes(json.as_bytes());
257
258 assert_eq!(sha256.len(), 64);
260 assert_eq!(sha3.len(), 128);
261
262 let (sha256_again, sha3_again) = generate_hashes(json.as_bytes());
264 assert_eq!(sha256, sha256_again);
265 assert_eq!(sha3, sha3_again);
266 }
267}