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("spawning curl", source))?;
if !status.success() {
return Err(Error::curl_failed(url, 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::checksum_mismatch(expected, 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(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(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())
}