1use std::io::Read;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use anyhow::{Context, Result, bail};
11
12use crate::progress::{Event, Reporter};
13use crate::shutdown::Cancel;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Algorithm {
17 Sha256,
18 Sha512,
19 Blake3,
20}
21
22impl Algorithm {
23 pub fn as_str(&self) -> &'static str {
24 match self {
25 Algorithm::Sha256 => "sha256",
26 Algorithm::Sha512 => "sha512",
27 Algorithm::Blake3 => "blake3",
28 }
29 }
30
31 pub fn hex_len(&self) -> usize {
34 match self {
35 Algorithm::Sha256 => 64,
36 Algorithm::Sha512 => 128,
37 Algorithm::Blake3 => 64,
38 }
39 }
40
41 pub fn label(&self) -> &'static str {
43 match self {
44 Algorithm::Sha256 => "SHA-256",
45 Algorithm::Sha512 => "SHA-512",
46 Algorithm::Blake3 => "BLAKE3",
47 }
48 }
49}
50
51impl std::str::FromStr for Algorithm {
52 type Err = anyhow::Error;
53
54 fn from_str(s: &str) -> Result<Self> {
55 match s.trim().to_ascii_lowercase().replace('-', "").as_str() {
56 "sha256" => Ok(Algorithm::Sha256),
57 "sha512" => Ok(Algorithm::Sha512),
58 "blake3" | "b3" => Ok(Algorithm::Blake3),
59 other => bail!("unknown checksum algorithm `{other}`"),
60 }
61 }
62}
63
64impl std::fmt::Display for Algorithm {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.write_str(self.as_str())
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Checksum {
72 pub algorithm: Algorithm,
73 pub expected: String,
74}
75
76impl Checksum {
77 pub fn parse(algorithm: Algorithm, raw: &str) -> Result<Self> {
80 let cleaned = raw
81 .trim()
82 .rsplit([':', '='])
83 .next()
84 .unwrap_or("")
85 .trim()
86 .to_ascii_lowercase();
87 if cleaned.len() != algorithm.hex_len() {
88 bail!(
89 "{} digest must be {} hex characters, got {}",
90 algorithm.label(),
91 algorithm.hex_len(),
92 cleaned.len()
93 );
94 }
95 if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
96 bail!("{} digest is not valid hex", algorithm.label());
97 }
98 Ok(Self {
99 algorithm,
100 expected: cleaned,
101 })
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum Outcome {
107 Match { actual: String },
108 Mismatch { expected: String, actual: String },
109}
110
111impl Outcome {
112 pub fn ok(&self) -> bool {
113 matches!(self, Outcome::Match { .. })
114 }
115
116 pub fn actual(&self) -> &str {
117 match self {
118 Outcome::Match { actual } | Outcome::Mismatch { actual, .. } => actual,
119 }
120 }
121}
122
123pub async fn verify(
126 path: &Path,
127 checksum: Checksum,
128 reporter: Reporter,
129 cancel: Cancel,
130) -> Result<Outcome> {
131 let path: PathBuf = path.to_path_buf();
132 let total = std::fs::metadata(&path)
133 .with_context(|| format!("cannot stat {}", path.display()))?
134 .len();
135
136 reporter.emit(Event::VerificationStarted {
137 algorithm: checksum.algorithm.as_str().to_string(),
138 total_size: total,
139 });
140
141 let actual = tokio::task::spawn_blocking({
142 let reporter = reporter.clone();
143 let algorithm = checksum.algorithm;
144 move || hash_file(&path, algorithm, total, &reporter, &cancel)
145 })
146 .await
147 .context("hashing task panicked")??;
148
149 let outcome = if actual == checksum.expected {
150 Outcome::Match { actual }
151 } else {
152 Outcome::Mismatch {
153 expected: checksum.expected.clone(),
154 actual,
155 }
156 };
157
158 reporter.emit(Event::VerificationCompleted {
159 algorithm: checksum.algorithm.as_str().to_string(),
160 ok: outcome.ok(),
161 expected: Some(checksum.expected),
162 actual: outcome.actual().to_string(),
163 });
164
165 Ok(outcome)
166}
167
168fn hash_file(
171 path: &Path,
172 algorithm: Algorithm,
173 total: u64,
174 reporter: &Reporter,
175 cancel: &Cancel,
176) -> Result<String> {
177 let mut file =
178 std::fs::File::open(path).with_context(|| format!("cannot read {}", path.display()))?;
179 let mut buf = vec![0u8; 1 << 20];
180 let mut hasher = Hasher::new(algorithm);
181 let mut read_total = 0u64;
182 let mut last_report = std::time::Instant::now();
183
184 loop {
185 if cancel.is_cancelled() {
186 bail!("verification cancelled");
187 }
188 let n = file.read(&mut buf).context("read failed while hashing")?;
189 if n == 0 {
190 break;
191 }
192 hasher.update(&buf[..n]);
193 read_total += n as u64;
194 if last_report.elapsed() >= std::time::Duration::from_millis(100) {
195 reporter.emit(Event::VerificationProgress {
196 bytes: read_total,
197 total_size: total,
198 });
199 last_report = std::time::Instant::now();
200 }
201 }
202
203 reporter.emit(Event::VerificationProgress {
204 bytes: read_total,
205 total_size: total,
206 });
207 Ok(hasher.finalize())
208}
209
210enum Hasher {
211 Sha256(sha2::Sha256),
212 Sha512(sha2::Sha512),
213 Blake3(Box<blake3::Hasher>),
214}
215
216impl Hasher {
217 fn new(algorithm: Algorithm) -> Self {
218 use sha2::Digest;
219 match algorithm {
220 Algorithm::Sha256 => Hasher::Sha256(sha2::Sha256::new()),
221 Algorithm::Sha512 => Hasher::Sha512(sha2::Sha512::new()),
222 Algorithm::Blake3 => Hasher::Blake3(Box::new(blake3::Hasher::new())),
223 }
224 }
225
226 fn update(&mut self, data: &[u8]) {
227 use sha2::Digest;
228 match self {
229 Hasher::Sha256(h) => h.update(data),
230 Hasher::Sha512(h) => h.update(data),
231 Hasher::Blake3(h) => {
232 h.update(data);
233 }
234 }
235 }
236
237 fn finalize(self) -> String {
238 use sha2::Digest;
239 match self {
240 Hasher::Sha256(h) => hex(&h.finalize()),
241 Hasher::Sha512(h) => hex(&h.finalize()),
242 Hasher::Blake3(h) => h.finalize().to_hex().to_string(),
243 }
244 }
245}
246
247fn hex(bytes: &[u8]) -> String {
248 let mut s = String::with_capacity(bytes.len() * 2);
249 for b in bytes {
250 use std::fmt::Write;
251 let _ = write!(s, "{b:02x}");
252 }
253 s
254}
255
256pub fn hash_bytes(algorithm: Algorithm, data: &[u8]) -> String {
258 let mut h = Hasher::new(algorithm);
259 h.update(data);
260 h.finalize()
261}
262
263pub type SharedChecksum = Arc<Checksum>;
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 const EMPTY_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
271 const ABC_SHA256: &str = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
272
273 #[test]
274 fn parses_algorithms() {
275 assert_eq!("sha256".parse::<Algorithm>().unwrap(), Algorithm::Sha256);
276 assert_eq!("SHA-512".parse::<Algorithm>().unwrap(), Algorithm::Sha512);
277 assert_eq!("blake3".parse::<Algorithm>().unwrap(), Algorithm::Blake3);
278 assert!("md5".parse::<Algorithm>().is_err());
279 }
280
281 #[test]
282 fn known_digests() {
283 assert_eq!(hash_bytes(Algorithm::Sha256, b""), EMPTY_SHA256);
284 assert_eq!(hash_bytes(Algorithm::Sha256, b"abc"), ABC_SHA256);
285 assert_eq!(
286 hash_bytes(Algorithm::Sha512, b"abc"),
287 "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a\
288 2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
289 );
290 assert_eq!(
291 hash_bytes(Algorithm::Blake3, b"abc"),
292 "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"
293 );
294 }
295
296 #[test]
297 fn checksum_parsing_is_forgiving_but_strict() {
298 let c = Checksum::parse(
299 Algorithm::Sha256,
300 &format!(" {} ", ABC_SHA256.to_uppercase()),
301 )
302 .unwrap();
303 assert_eq!(c.expected, ABC_SHA256);
304
305 let c = Checksum::parse(Algorithm::Sha256, &format!("sha256:{ABC_SHA256}")).unwrap();
306 assert_eq!(c.expected, ABC_SHA256);
307
308 assert!(Checksum::parse(Algorithm::Sha256, "abc").is_err());
310 assert!(Checksum::parse(Algorithm::Sha256, &"a".repeat(65)).is_err());
311 assert!(Checksum::parse(Algorithm::Sha256, &"z".repeat(64)).is_err());
312 assert!(Checksum::parse(Algorithm::Sha512, ABC_SHA256).is_err());
314 }
315
316 #[tokio::test]
317 async fn verifies_a_file_and_reports_progress() {
318 let dir = std::env::temp_dir().join(format!("rget-integrity-{}", std::process::id()));
319 std::fs::create_dir_all(&dir).unwrap();
320 let path = dir.join("data.bin");
321 let data: Vec<u8> = (0..(3 << 20)).map(|i| (i % 251) as u8).collect();
323 std::fs::write(&path, &data).unwrap();
324 let expected = hash_bytes(Algorithm::Sha256, &data);
325
326 let (reporter, mut rx) = Reporter::new();
327 let outcome = verify(
328 &path,
329 Checksum::parse(Algorithm::Sha256, &expected).unwrap(),
330 reporter,
331 Cancel::new(),
332 )
333 .await
334 .unwrap();
335 assert!(outcome.ok());
336
337 let mut saw_started = false;
338 let mut final_progress = 0;
339 let mut completed_ok = None;
340 while let Ok(ev) = rx.try_recv() {
341 match ev {
342 Event::VerificationStarted { total_size, .. } => {
343 saw_started = true;
344 assert_eq!(total_size, data.len() as u64);
345 }
346 Event::VerificationProgress { bytes, .. } => final_progress = bytes,
347 Event::VerificationCompleted { ok, .. } => completed_ok = Some(ok),
348 _ => {}
349 }
350 }
351 assert!(saw_started);
352 assert_eq!(final_progress, data.len() as u64);
353 assert_eq!(completed_ok, Some(true));
354
355 std::fs::remove_dir_all(&dir).ok();
356 }
357
358 #[tokio::test]
359 async fn mismatch_is_reported_not_swallowed() {
360 let dir = std::env::temp_dir().join(format!("rget-integrity-bad-{}", std::process::id()));
361 std::fs::create_dir_all(&dir).unwrap();
362 let path = dir.join("data.bin");
363 std::fs::write(&path, b"abc").unwrap();
364
365 let outcome = verify(
366 &path,
367 Checksum::parse(Algorithm::Sha256, EMPTY_SHA256).unwrap(),
368 Reporter::silent(),
369 Cancel::new(),
370 )
371 .await
372 .unwrap();
373
374 assert!(!outcome.ok());
375 match outcome {
376 Outcome::Mismatch { expected, actual } => {
377 assert_eq!(expected, EMPTY_SHA256);
378 assert_eq!(actual, ABC_SHA256);
379 }
380 Outcome::Match { .. } => panic!("must not report a match"),
381 }
382 std::fs::remove_dir_all(&dir).ok();
383 }
384}