idt/downloaders/
checksum.rs1use std::fmt::Write;
2use std::fs::File;
3use std::io::Read;
4use std::path::Path;
5
6use rootcause::prelude::ResultExt as _;
7use rootcause::report;
8use sha2::Digest as _;
9
10pub fn compute_sha256(path: &Path) -> rootcause::Result<String> {
15 let mut file = File::open(path)
16 .context("error opening file for checksum")
17 .attach_with(|| format!("path={}", path.display()))?;
18
19 let mut hasher = sha2::Sha256::new();
20 let mut buf = [0_u8; 8192];
21
22 loop {
23 let n = file
24 .read(&mut buf)
25 .context("error reading file for checksum")
26 .attach_with(|| format!("path={}", path.display()))?;
27 if n == 0 {
28 break;
29 }
30 hasher.update(
31 buf.get(..n)
32 .ok_or_else(|| report!("error slicing buffer"))
33 .attach_with(|| format!("n={n} buf_len={}", buf.len()))?,
34 );
35 }
36
37 let digest = hasher.finalize();
38 let mut digest_hex = String::new();
39
40 for byte in digest {
41 write!(&mut digest_hex, "{byte:02x}")
42 .map_err(|err| report!("error formatting checksum").attach(format!("error={err:?}")))?;
43 }
44
45 Ok(digest_hex)
46}
47
48pub fn download_and_find_checksum(checksums_url: &str, filename: &str) -> rootcause::Result<String> {
58 let body = ureq::get(checksums_url)
59 .call()
60 .context("error downloading checksums file")
61 .attach_with(|| format!("url={checksums_url}"))?
62 .into_body()
63 .read_to_string()
64 .context("error reading checksums response")
65 .attach_with(|| format!("url={checksums_url}"))?;
66
67 parse_checksum(&body, filename)
68}
69
70fn parse_checksum(content: &str, filename: &str) -> rootcause::Result<String> {
79 let trimmed = content.trim();
80
81 if !trimmed.contains(' ') && !trimmed.contains('\n') && !trimmed.is_empty() {
83 return Ok(trimmed.to_owned());
84 }
85
86 for line in trimmed.lines() {
88 let line = line.trim();
89 if let Some((hash, rest)) = line.split_once(' ') {
91 let rest = rest.trim_start();
92 let entry_filename = rest.strip_prefix('*').unwrap_or(rest);
94 if entry_filename == filename {
95 return Ok(hash.to_owned());
96 }
97 }
98 }
99
100 Err(report!("error checksum entry not found").attach(format!("filename={filename:?} content={trimmed:?}")))
101}
102
103pub fn verify(path: &Path, expected_hex: &str) -> rootcause::Result<()> {
109 let actual = compute_sha256(path)?;
110 let expected = expected_hex.to_lowercase();
111
112 if actual != expected {
113 return Err(report!("error checksum mismatch")
114 .attach(format!("path={} expected={expected} actual={actual}", path.display())));
115 }
116
117 Ok(())
118}
119
120#[cfg(test)]
121mod tests {
122 use rstest::rstest;
123
124 use super::*;
125
126 #[rstest]
127 #[case::multi_line_format("abc123 foo.tar.gz\ndef456 bar.zip\n", "bar.zip", "def456")]
128 #[case::single_space("abc123 foo.tar.gz\n", "foo.tar.gz", "abc123")]
129 #[case::binary_mode_indicator("abc123 *foo.tar.gz\n", "foo.tar.gz", "abc123")]
130 #[case::single_line_hash("abc123def456\n", "anything", "abc123def456")]
131 fn test_parse_checksum_returns_expected_hash(
132 #[case] content: &str,
133 #[case] filename: &str,
134 #[case] expected: &str,
135 ) {
136 assert2::assert!(let Ok(hash) = parse_checksum(content, filename));
137 pretty_assertions::assert_eq!(hash, expected);
138 }
139
140 #[test]
141 fn test_parse_checksum_returns_error_when_not_found() {
142 let content = "abc123 foo.tar.gz\ndef456 bar.zip\n";
143 assert2::assert!(let Err(err) = parse_checksum(content, "missing.txt"));
144 assert!(err.to_string().contains("error checksum entry not found"));
145 }
146
147 #[test]
148 fn test_compute_sha256_returns_expected_hash() {
149 let dir = tempfile::tempdir().unwrap();
150 let file_path = dir.path().join("test.txt");
151 std::fs::write(&file_path, b"hello world").unwrap();
152
153 assert2::assert!(let Ok(hash) = compute_sha256(&file_path));
154 pretty_assertions::assert_eq!(hash, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
155 }
156
157 #[test]
158 fn test_verify_succeeds_with_matching_hash() {
159 let dir = tempfile::tempdir().unwrap();
160 let file_path = dir.path().join("test.txt");
161 std::fs::write(&file_path, b"hello world").unwrap();
162
163 assert2::assert!(let
164 Ok(()) = verify(
165 &file_path,
166 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
167 )
168 );
169 }
170
171 #[test]
172 fn test_verify_fails_with_mismatched_hash() {
173 let dir = tempfile::tempdir().unwrap();
174 let file_path = dir.path().join("test.txt");
175 std::fs::write(&file_path, b"hello world").unwrap();
176
177 assert2::assert!(let
178 Err(err) = verify(
179 &file_path,
180 "0000000000000000000000000000000000000000000000000000000000000000"
181 )
182 );
183 assert!(err.to_string().contains("error checksum mismatch"));
184 }
185}