Skip to main content

cloud_image_download/
checksums.rs

1use 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/// An enum to allow different checksum types
11/// For now there is only sha256 and sha512
12/// all other cases is a `CheckSums::None`
13#[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
23/// Generic hash verification helper
24///
25/// # Errors
26///
27/// It will return errors when
28///  - the file can not be opened
29///  - the file can not be read
30fn 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    /// Builds a `CheckSums` structure with the checksum found in
60    /// the line
61    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    /// retrieves the checksum of the image named `name` in the buffer
87    /// `checksums` that is the content of a file containing at least
88    /// one checksum. `filename` is the filename of that file containing
89    /// all checksums. We decide with its name the kind of checksums
90    /// it contains (sha256 or sha512) along with the content of the
91    /// line that may also be helpful
92    #[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    /// Verifies a file's (named `filename`) checksum (contained in `checksum`)
117    ///
118    /// # Errors
119    ///
120    /// It will return errors when
121    ///  - the file cannot be opened
122    ///  - the file cannot be read
123    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/// Tells if inner String indicates that we are
153/// in presence of a checksum files that contains
154/// all checksums for all downloadable images
155#[must_use]
156pub fn are_all_checksums_in_one_file(inner: &str) -> bool {
157    // -CHECKSUM is used in Fedora sites
158    // CHECKSUM is used in Centos sites
159    // SHA256SUMS is used in Ubuntu sites
160    // SHA512SUMS is used in Debian sites
161    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    /// Helper function to create a temporary file with given content
171    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        // SHA256 hash of "Hello, World!"
193        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        // Incorrect hash
208        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        // SHA512 hash of "Hello, World!"
223        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        // Incorrect hash
238        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        // SHA256 hash of empty string
260        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        // Create a file larger than buffer size (16MB + some extra)
272        let large_content = vec![b'A'; 17_000_000];
273        let file = create_temp_file(&large_content);
274
275        // SHA256 hash of 17MB of 'A' characters
276        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}