idt/downloaders/
checksum.rs1use std::fs::File;
2use std::io::Read;
3use std::path::Path;
4
5use rootcause::prelude::ResultExt as _;
6use rootcause::report;
7use sha2::Digest as _;
8
9pub fn compute_sha256(path: &Path) -> rootcause::Result<String> {
14 let mut file = File::open(path)
15 .context("error opening file for checksum")
16 .attach_with(|| format!("path={}", path.display()))?;
17
18 let mut hasher = sha2::Sha256::new();
19 let mut buf = [0_u8; 8192];
20
21 loop {
22 let n = file
23 .read(&mut buf)
24 .context("error reading file for checksum")
25 .attach_with(|| format!("path={}", path.display()))?;
26 if n == 0 {
27 break;
28 }
29 hasher.update(
30 buf.get(..n)
31 .ok_or_else(|| report!("error slicing buffer"))
32 .attach_with(|| format!("n={n} buf_len={}", buf.len()))?,
33 );
34 }
35
36 Ok(format!("{:x}", hasher.finalize()))
37}
38
39pub fn download_and_find_checksum(checksums_url: &str, filename: &str) -> rootcause::Result<String> {
49 let body = ureq::get(checksums_url)
50 .call()
51 .context("error downloading checksums file")
52 .attach_with(|| format!("url={checksums_url}"))?
53 .into_body()
54 .read_to_string()
55 .context("error reading checksums response")
56 .attach_with(|| format!("url={checksums_url}"))?;
57
58 parse_checksum(&body, filename)
59}
60
61fn parse_checksum(content: &str, filename: &str) -> rootcause::Result<String> {
70 let trimmed = content.trim();
71
72 if !trimmed.contains(' ') && !trimmed.contains('\n') && !trimmed.is_empty() {
74 return Ok(trimmed.to_owned());
75 }
76
77 for line in trimmed.lines() {
79 let line = line.trim();
80 if let Some((hash, rest)) = line.split_once(' ') {
82 let rest = rest.trim_start();
83 let entry_filename = rest.strip_prefix('*').unwrap_or(rest);
85 if entry_filename == filename {
86 return Ok(hash.to_owned());
87 }
88 }
89 }
90
91 Err(report!("error checksum entry not found").attach(format!("filename={filename:?} content={trimmed:?}")))
92}
93
94pub fn verify(path: &Path, expected_hex: &str) -> rootcause::Result<()> {
100 let actual = compute_sha256(path)?;
101 let expected = expected_hex.to_lowercase();
102
103 if actual != expected {
104 return Err(report!("error checksum mismatch")
105 .attach(format!("path={} expected={expected} actual={actual}", path.display())));
106 }
107
108 Ok(())
109}
110
111#[cfg(test)]
112mod tests {
113 use rstest::rstest;
114
115 use super::*;
116
117 #[rstest]
118 #[case::multi_line_format("abc123 foo.tar.gz\ndef456 bar.zip\n", "bar.zip", "def456")]
119 #[case::single_space("abc123 foo.tar.gz\n", "foo.tar.gz", "abc123")]
120 #[case::binary_mode_indicator("abc123 *foo.tar.gz\n", "foo.tar.gz", "abc123")]
121 #[case::single_line_hash("abc123def456\n", "anything", "abc123def456")]
122 fn parse_checksum_returns_expected_hash(#[case] content: &str, #[case] filename: &str, #[case] expected: &str) {
123 assert2::assert!(let Ok(hash) = parse_checksum(content, filename));
124 pretty_assertions::assert_eq!(hash, expected);
125 }
126
127 #[test]
128 fn parse_checksum_returns_error_when_not_found() {
129 let content = "abc123 foo.tar.gz\ndef456 bar.zip\n";
130 assert2::assert!(let Err(err) = parse_checksum(content, "missing.txt"));
131 assert!(err.to_string().contains("error checksum entry not found"));
132 }
133
134 #[test]
135 fn compute_sha256_returns_expected_hash() {
136 let dir = tempfile::tempdir().unwrap();
137 let file_path = dir.path().join("test.txt");
138 std::fs::write(&file_path, b"hello world").unwrap();
139
140 assert2::assert!(let Ok(hash) = compute_sha256(&file_path));
141 pretty_assertions::assert_eq!(hash, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
142 }
143
144 #[test]
145 fn verify_succeeds_with_matching_hash() {
146 let dir = tempfile::tempdir().unwrap();
147 let file_path = dir.path().join("test.txt");
148 std::fs::write(&file_path, b"hello world").unwrap();
149
150 assert2::assert!(let
151 Ok(()) = verify(
152 &file_path,
153 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
154 )
155 );
156 }
157
158 #[test]
159 fn verify_fails_with_mismatched_hash() {
160 let dir = tempfile::tempdir().unwrap();
161 let file_path = dir.path().join("test.txt");
162 std::fs::write(&file_path, b"hello world").unwrap();
163
164 assert2::assert!(let
165 Err(err) = verify(
166 &file_path,
167 "0000000000000000000000000000000000000000000000000000000000000000"
168 )
169 );
170 assert!(err.to_string().contains("error checksum mismatch"));
171 }
172}