xtask_lib/tasks/util/
http.rs

1//! HTTP download and checksum helpers.
2
3use std::path::Path;
4
5use anyhow::{Context, Result, bail};
6use sha2::{Digest, Sha256};
7
8/// Downloads `url` to `dest`, following redirects.
9///
10/// Creates parent directories of `dest` if they do not exist.
11///
12/// # Errors
13///
14/// Returns an error if the HTTP request fails, the server returns a non-success
15/// status, or writing to `dest` fails.
16pub fn download(url: &str, dest: &Path) -> Result<()> {
17    if let Some(parent) = dest.parent() {
18        std::fs::create_dir_all(parent)
19            .with_context(|| format!("failed to create parent directory for {}", dest.display()))?;
20    }
21
22    let response = reqwest::blocking::get(url)
23        .with_context(|| format!("HTTP request failed for {url}"))?
24        .error_for_status()
25        .with_context(|| format!("server returned error status for {url}"))?;
26
27    let bytes = response
28        .bytes()
29        .with_context(|| format!("failed to read response body from {url}"))?;
30
31    std::fs::write(dest, &bytes)
32        .with_context(|| format!("failed to write downloaded file to {}", dest.display()))
33}
34
35/// Verifies the SHA-256 checksum of `file` against `expected`.
36///
37/// `expected` must be a lowercase hex string without any prefix.
38///
39/// # Errors
40///
41/// Returns an error if the file cannot be read or the checksum does not match.
42pub fn verify_sha256(file: &Path, expected: &str) -> Result<()> {
43    let bytes = std::fs::read(file).with_context(|| {
44        format!(
45            "failed to read {} for checksum verification",
46            file.display()
47        )
48    })?;
49
50    let mut hasher = Sha256::new();
51    hasher.update(&bytes);
52    let actual: String = hasher
53        .finalize()
54        .iter()
55        .map(|b| format!("{b:02x}"))
56        .collect();
57
58    if actual != expected {
59        bail!(
60            "SHA-256 checksum mismatch for {}\n  expected: {expected}\n  got:      {actual}",
61            file.display()
62        );
63    }
64
65    println!("Checksum verified.");
66
67    Ok(())
68}
69
70/// Downloads `url` to a temporary file, verifies its SHA-256 checksum, then
71/// moves it to `dest`.
72///
73/// # Errors
74///
75/// Returns an error if the download, checksum verification, or move fails.
76pub fn download_verified(url: &str, dest: &Path, expected_sha256: &str) -> Result<()> {
77    let tmp = dest.with_extension("tmp");
78    download(url, &tmp)?;
79
80    if let Err(e) = verify_sha256(&tmp, expected_sha256) {
81        std::fs::remove_file(&tmp).ok();
82        return Err(e);
83    }
84
85    std::fs::rename(&tmp, dest)
86        .with_context(|| format!("failed to move download to {}", dest.display()))
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use std::fs;
93
94    #[test]
95    fn verify_sha256_accepts_correct_hash() {
96        let tmp = tempfile::tempdir().unwrap();
97        let file = tmp.path().join("data.bin");
98        fs::write(&file, b"hello world").unwrap();
99
100        let mut hasher = Sha256::new();
101        hasher.update(b"hello world");
102        let real_hash: String = hasher
103            .finalize()
104            .iter()
105            .map(|b| format!("{b:02x}"))
106            .collect();
107
108        assert!(verify_sha256(&file, &real_hash).is_ok());
109    }
110
111    #[test]
112    fn verify_sha256_rejects_wrong_hash() {
113        let tmp = tempfile::tempdir().unwrap();
114        let file = tmp.path().join("data.bin");
115        fs::write(&file, b"hello world").unwrap();
116
117        let result = verify_sha256(
118            &file,
119            "0000000000000000000000000000000000000000000000000000000000000000",
120        );
121        assert!(result.is_err());
122        let msg = format!("{}", result.unwrap_err());
123        assert!(msg.contains("checksum mismatch"));
124    }
125}