use std::io::Read as _;
use std::path::PathBuf;
use sha2::Digest as _;
use crate as cindy;
use crate::Context;
#[derive(Clone, PartialEq, Eq)]
#[crate::wire]
pub enum Checksum {
Sha256(String),
Sha512(String),
}
impl Checksum {
fn expected(&self) -> String {
match self {
Checksum::Sha256(h) | Checksum::Sha512(h) => h.trim().to_ascii_lowercase(),
}
}
fn digest(&self, bytes: &[u8]) -> String {
match self {
Checksum::Sha256(_) => hex(&sha2::Sha256::digest(bytes)),
Checksum::Sha512(_) => hex(&sha2::Sha512::digest(bytes)),
}
}
fn matches(&self, bytes: &[u8]) -> bool {
self.digest(bytes) == self.expected()
}
}
fn hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes.iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
#[derive(Clone, Default, PartialEq, Eq)]
#[crate::wire]
pub struct State {
pub url: String,
pub dest: PathBuf,
pub checksum: Option<Checksum>,
pub user: Option<String>,
pub group: Option<String>,
pub mode: Option<super::path::Mode>,
pub force: bool,
}
fn already_satisfied(state: &State) -> bool {
if state.force {
return false;
}
let Ok(existing) = std::fs::read(&state.dest) else {
return false;
};
match &state.checksum {
Some(sum) => sum.matches(&existing),
None => true,
}
}
fn download(url: &str) -> crate::Result<Vec<u8>> {
let mut response = ureq::get(url).call().context(format!("GET {url} failed"))?;
let mut body = Vec::new();
response
.body_mut()
.as_reader()
.read_to_end(&mut body)
.context(format!("Reading response body from {url} failed"))?;
Ok(body)
}
#[crate::remote]
pub fn fetch(state: State) -> crate::Result<super::Return> {
if already_satisfied(&state) {
return Ok(super::Return::Unchanged);
}
eprintln!("fetch {} -> {}", state.url, state.dest.display());
let body = download(&state.url)?;
if let Some(sum) = &state.checksum
&& !sum.matches(&body)
{
crate::bail!(
"checksum mismatch for {}: expected {}, got {}",
state.url,
sum.expected(),
sum.digest(&body),
);
}
let (user, group) = super::current_owner_names();
super::path::file_raw::inner(
state.dest,
body,
state.user.unwrap_or(user),
state.group.unwrap_or(group),
state.mode.unwrap_or_else(|| 0o644.into()),
)
}