cloud_image_download/
checksums.rs1use base16ct::lower;
2use log::{debug, error, info, trace, warn};
3use regex::Regex;
4use sha2::{Digest, Sha256, Sha512};
5use std::error::Error;
6use std::fmt;
7use std::fs::File;
8use std::io::{BufReader, Read};
9
10#[derive(PartialEq, Default, Debug)]
14pub enum CheckSums {
15 Sha256(String),
16 Sha512(String),
17 #[default]
18 None,
19}
20
21const HASH_BUFFER_SIZE: usize = 16_777_216;
22
23fn verify_with_hasher<D: Digest>(
31 filename: &str,
32 mut hasher: D,
33 expected_hash: &str,
34) -> Result<Option<bool>, Box<dyn Error>> {
35 let input = File::open(filename).map_err(|e| {
36 error!("Error while opening {filename}: {e}");
37 e
38 })?;
39
40 let mut reader = BufReader::new(input);
41 let mut buffer = vec![0; HASH_BUFFER_SIZE];
42
43 loop {
44 let count = reader.read(&mut buffer).inspect_err(|e| {
45 error!("Error while reading file {filename} skipped: {e}");
46 })?;
47
48 if count == 0 {
49 break;
50 }
51 hasher.update(&buffer[..count]);
52 }
53
54 let digest = hasher.finalize();
55 Ok(Some(lower::encode_string(&digest) == expected_hash))
56}
57
58impl CheckSums {
59 fn build_checksums_from_line(line: &str, filename: &str) -> CheckSums {
62 if filename.contains("SHA512SUMS") || line.contains("SHA512") {
63 let re = Regex::new(r".*([a-f0-9]{128}+).*")
64 .expect("Something went wrong because '.*([a-f0-9]{128}+).*' is a valid regex");
65 let chksum: String = match re.captures(line) {
66 Some(value) => value[1].to_string(),
67 None => return CheckSums::None,
68 };
69 info!("found sha512 checksum '{chksum}'");
70 CheckSums::Sha512(chksum)
71 } else if filename.contains("SHA256SUMS") || line.contains("SHA256") {
72 let re = Regex::new(r".*([a-f0-9]{64}+).*")
73 .expect("Something went wrong because '.*([a-f0-9]{64}+).*' is a valid regex");
74 let chksum: String = match re.captures(line) {
75 Some(value) => value[1].to_string(),
76 None => return CheckSums::None,
77 };
78 info!("found sha256 checksum '{chksum}'");
79 CheckSums::Sha256(chksum)
80 } else {
81 info!("no checksum found (not a SHA256 or SHA512 ?)");
82 CheckSums::None
83 }
84 }
85
86 #[must_use]
93 pub fn get_image_checksum_from_checksums_buffer(
94 name: &str,
95 checksums: &Option<String>,
96 filename: &str,
97 ) -> CheckSums {
98 match checksums {
99 Some(buffer) => {
100 for line in buffer.lines() {
101 if !line.is_empty() && !line.starts_with('#') {
102 trace!("line: {line}");
103 if line.contains(name) {
104 debug!("line: {line}");
105 return CheckSums::build_checksums_from_line(line, filename);
106 }
107 }
108 }
109 info!("no checksum found");
110 }
111 None => info!("no checksum buffer to analyze"),
112 }
113 CheckSums::None
114 }
115
116 pub fn verify_file(&self, filename: &str) -> Result<Option<bool>, Box<dyn Error>> {
124 match self {
125 CheckSums::None => {
126 warn!("No checksum for file {filename}: nothing verified");
127 Ok(None)
128 }
129 CheckSums::Sha256(hash) => {
130 info!("Verifying {filename} sha256's checksum");
131 verify_with_hasher(filename, Sha256::new(), hash)
132 }
133 CheckSums::Sha512(hash) => {
134 info!("Verifying {filename} sha512's checksum");
135 verify_with_hasher(filename, Sha512::new(), hash)
136 }
137 }
138 }
139}
140
141impl fmt::Display for CheckSums {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match &self {
144 CheckSums::None => writeln!(f),
145 CheckSums::Sha256(checksum) | CheckSums::Sha512(checksum) => {
146 writeln!(f, "{checksum}")
147 }
148 }
149 }
150}
151
152#[must_use]
156pub fn are_all_checksums_in_one_file(inner: &str) -> bool {
157 inner.contains("-CHECKSUM") || inner == "CHECKSUM" || inner == "SHA256SUMS" || inner == "SHA512SUMS"
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::io::Write;
168 use tempfile::NamedTempFile;
169
170 fn create_temp_file(content: &[u8]) -> NamedTempFile {
172 let mut file = NamedTempFile::new().expect("Failed to create temp file");
173 file.write_all(content).expect("Failed to write to temp file");
174 file.flush().expect("Failed to flush temp file");
175 file
176 }
177
178 #[test]
179 fn test_verify_file_none_checksum() {
180 let file = create_temp_file(b"test content");
181 let result = CheckSums::None.verify_file(file.path().to_str().unwrap());
182
183 assert!(result.is_ok());
184 assert_eq!(result.unwrap(), None);
185 }
186
187 #[test]
188 fn test_verify_file_sha256_valid() {
189 let content = b"Hello, World!";
190 let file = create_temp_file(content);
191
192 let expected_hash = "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f";
194 let checksum = CheckSums::Sha256(expected_hash.to_string());
195
196 let result = checksum.verify_file(file.path().to_str().unwrap());
197
198 assert!(result.is_ok());
199 assert_eq!(result.unwrap(), Some(true));
200 }
201
202 #[test]
203 fn test_verify_file_sha256_invalid() {
204 let content = b"Hello, World!";
205 let file = create_temp_file(content);
206
207 let wrong_hash = "0000000000000000000000000000000000000000000000000000000000000000";
209 let checksum = CheckSums::Sha256(wrong_hash.to_string());
210
211 let result = checksum.verify_file(file.path().to_str().unwrap());
212
213 assert!(result.is_ok());
214 assert_eq!(result.unwrap(), Some(false));
215 }
216
217 #[test]
218 fn test_verify_file_sha512_valid() {
219 let content = b"Hello, World!";
220 let file = create_temp_file(content);
221
222 let expected_hash = "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387";
224 let checksum = CheckSums::Sha512(expected_hash.to_string());
225
226 let result = checksum.verify_file(file.path().to_str().unwrap());
227
228 assert!(result.is_ok());
229 assert_eq!(result.unwrap(), Some(true));
230 }
231
232 #[test]
233 fn test_verify_file_sha512_invalid() {
234 let content = b"Hello, World!";
235 let file = create_temp_file(content);
236
237 let wrong_hash = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
239 let checksum = CheckSums::Sha512(wrong_hash.to_string());
240
241 let result = checksum.verify_file(file.path().to_str().unwrap());
242
243 assert!(result.is_ok());
244 assert_eq!(result.unwrap(), Some(false));
245 }
246
247 #[test]
248 fn test_verify_file_nonexistent() {
249 let checksum = CheckSums::Sha256("dummy_hash".to_string());
250 let result = checksum.verify_file("/nonexistent/file/path.txt");
251
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn test_verify_file_empty_file() {
257 let file = create_temp_file(b"");
258
259 let expected_hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
261 let checksum = CheckSums::Sha256(expected_hash.to_string());
262
263 let result = checksum.verify_file(file.path().to_str().unwrap());
264
265 assert!(result.is_ok());
266 assert_eq!(result.unwrap(), Some(true));
267 }
268
269 #[test]
270 fn test_verify_file_large_content() {
271 let large_content = vec![b'A'; 17_000_000];
273 let file = create_temp_file(&large_content);
274
275 let expected_hash = "3e4d2911aa103ff4e2d19f5180d10b099469826f182f8ebc7abd292896ec3fa3";
277 let checksum = CheckSums::Sha256(expected_hash.to_string());
278
279 let result = checksum.verify_file(file.path().to_str().unwrap());
280
281 assert!(result.is_ok());
282 assert_eq!(result.unwrap(), Some(true));
283 }
284
285 #[test]
286 fn test_verify_with_hasher_directly() {
287 let content = b"Test content";
288 let file = create_temp_file(content);
289
290 let expected_hash = "9d9595c5d94fb65b824f56e9999527dba9542481580d69feb89056aabaa0aa87";
291
292 let result = verify_with_hasher(file.path().to_str().unwrap(), Sha256::new(), expected_hash);
293
294 assert!(result.is_ok());
295 assert_eq!(result.unwrap(), Some(true));
296 }
297}