use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::process::Command;
use crate::Error;
const HASH_CHUNK: usize = 64 * 1024;
pub fn download_and_verify(
url: &str,
expected_sha256: Option<&str>,
dest: &Path,
) -> Result<(), Error> {
tracing::info!(url, dest = %dest.display(), "downloading");
let status = Command::new("curl")
.args([
"--fail",
"--silent",
"--show-error",
"--location",
"--output",
])
.arg(dest)
.arg(url)
.status()
.map_err(|source| Error::Io {
context: "spawning curl".to_string(),
source,
})?;
if !status.success() {
return Err(Error::CurlFailed {
url: url.to_string(),
code: status.code().unwrap_or(-1),
});
}
let actual = hash_file(dest)?;
match expected_sha256 {
Some(expected) => {
if !actual.eq_ignore_ascii_case(expected) {
return Err(Error::ChecksumMismatch {
expected: expected.to_string(),
actual,
});
}
tracing::info!(sha256 = %actual, "sha256 verified");
}
None => {
tracing::warn!(
sha256 = %actual,
"sha256 verification skipped (no expected hash supplied)"
);
}
}
Ok(())
}
#[expect(
clippy::format_collect,
reason = "readable hex/string formatting loop; refactoring to fold! obscures intent"
)]
fn hash_file(path: &Path) -> Result<String, Error> {
let mut file = File::open(path).map_err(|source| Error::Io {
context: format!("opening {} for hashing", path.display()),
source,
})?;
let mut hasher = Sha256::new();
let mut buf = vec![0u8; HASH_CHUNK];
loop {
let n = file.read(&mut buf).map_err(|source| Error::Io {
context: format!("reading {}", path.display()),
source,
})?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let digest = hasher.finalize();
Ok(digest.iter().map(|b| format!("{b:02x}")).collect())
}