use std::fmt::Write as _;
use std::fs::File;
use std::io::Write as _;
use std::time::Duration;
use sha2::{Digest, Sha256};
use crate::manifest::ArchiveInfo;
use crate::{Error, Result};
pub(crate) const MANIFEST_MAX_BYTES: u64 = 64 * 1024;
pub(crate) const SIGNATURE_MAX_BYTES: u64 = 8 * 1024;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub type OnProgress<'a> = &'a mut (dyn FnMut(u64, u64) + Send);
pub(crate) fn http_client() -> Result<reqwest::Client> {
let builder = reqwest::Client::builder().connect_timeout(CONNECT_TIMEOUT);
#[cfg(feature = "rustls-tls")]
let builder = {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
let mut roots = rustls::RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let tls = rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
builder.use_preconfigured_tls(tls)
};
Ok(builder.build()?)
}
pub(crate) async fn fetch_capped(client: &reqwest::Client, url: &str, cap: u64) -> Result<Vec<u8>> {
let mut response = checked_get(client, url).await?;
let mut body: Vec<u8> = Vec::new();
while let Some(chunk) = response.chunk().await? {
if body.len() as u64 + chunk.len() as u64 > cap {
return Err(Error::ResponseTooLarge {
url: url.to_string(),
limit: cap,
});
}
body.extend_from_slice(&chunk);
}
Ok(body)
}
pub(crate) async fn download_archive(
client: &reqwest::Client,
archive: &ArchiveInfo,
dest: &mut File,
on_progress: OnProgress<'_>,
) -> Result<()> {
let mut response = checked_get(client, &archive.url).await?;
let mut hasher = Sha256::new();
let mut downloaded: u64 = 0;
while let Some(chunk) = response.chunk().await? {
downloaded += chunk.len() as u64;
if downloaded > archive.size {
return Err(Error::ArchiveSize {
declared: archive.size,
actual: downloaded,
});
}
hasher.update(&chunk);
dest.write_all(&chunk)?;
on_progress(downloaded, archive.size);
}
if downloaded != archive.size {
return Err(Error::ArchiveSize {
declared: archive.size,
actual: downloaded,
});
}
let actual = to_hex(&hasher.finalize());
if actual != archive.sha256 {
return Err(Error::ArchiveSha256 {
declared: archive.sha256.clone(),
actual,
});
}
dest.flush()?;
Ok(())
}
async fn checked_get(client: &reqwest::Client, url: &str) -> Result<reqwest::Response> {
let response = client.get(url).send().await?;
let status = response.status();
if !status.is_success() {
return Err(Error::HttpStatus {
status: status.as_u16(),
url: url.to_string(),
});
}
Ok(response)
}
pub(crate) fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(out, "{b:02x}");
}
out
}