use crate::{
models::{common::enums::Provider, provider::Release},
providers::provider_manager::ProviderManager,
};
use anyhow::{Result, anyhow};
use std::{
fs,
path::{Path, PathBuf},
};
enum HashAlgo {
Sha256,
Sha512,
}
struct ChecksumEntry {
algo: HashAlgo,
filename: String,
digest: String,
}
pub struct ChecksumVerifier<'a> {
provider_manager: &'a ProviderManager,
download_cache: &'a Path,
}
impl<'a> ChecksumVerifier<'a> {
pub fn new(provider_manager: &'a ProviderManager, download_cache: &'a Path) -> Self {
Self {
provider_manager,
download_cache,
}
}
pub async fn try_verify_file<F>(
&self,
asset_path: &Path,
release: &Release,
provider: &Provider,
dl_progress: &mut Option<F>,
) -> Result<bool>
where
F: FnMut(u64, u64),
{
let asset_filename = asset_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow!("Invalid asset filename"))?;
let checksum_path = match self
.try_download_checksum(release, asset_filename, provider, dl_progress)
.await?
{
Some(path) => path,
None => return Ok(false), };
let contents = fs::read_to_string(&checksum_path)?;
let entries = Self::parse_checksums(&contents);
if entries.is_empty() {
return Err(anyhow!("Checksum file is empty or invalid"));
}
let checksum_entry = if entries.len() == 1 && entries[0].filename.is_empty() {
&entries[0]
} else {
entries
.iter()
.find(|entry| entry.filename == asset_filename)
.or_else(|| {
entries.iter().find(|entry| {
Path::new(&entry.filename)
.file_name()
.and_then(|n| n.to_str())
== Some(asset_filename)
})
})
.ok_or_else(|| {
anyhow!(
"No checksum found for asset '{}' in checksum file",
asset_filename
)
})?
};
Self::verify_checksum(asset_path, checksum_entry)
}
async fn try_download_checksum<F>(
&self,
release: &Release,
asset_name: &str,
provider: &Provider,
dl_progress: &mut Option<F>,
) -> Result<Option<PathBuf>>
where
F: FnMut(u64, u64),
{
let checksum_asset = release
.get_asset_by_name("checksums.txt")
.or_else(|| release.get_asset_by_name("sha256sums.txt"))
.or_else(|| release.get_asset_by_name("sha256sum.txt"))
.or_else(|| {
let name = format!("{asset_name}.sha256");
release.get_asset_by_name(&name)
});
let Some(asset) = checksum_asset else {
return Ok(None); };
let path = self
.provider_manager
.download_asset(asset, provider, self.download_cache, dl_progress)
.await?;
Ok(Some(path))
}
fn parse_checksums(contents: &str) -> Vec<ChecksumEntry> {
let mut entries = Vec::new();
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(entry) = Self::parse_standard_format(line) {
entries.push(entry);
} else if let Some(entry) = Self::parse_colon_format(line) {
entries.push(entry);
} else if let Some(entry) = Self::parse_bare_hash(line) {
entries.push(entry);
}
}
entries
}
fn parse_standard_format(line: &str) -> Option<ChecksumEntry> {
let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace()).collect();
if parts.len() != 2 {
return None;
}
let digest = parts[0].trim();
let filename = parts[1].trim().trim_start_matches('*').trim();
if digest.is_empty() || filename.is_empty() {
return None;
}
let algo = match digest.len() {
64 => HashAlgo::Sha256,
128 => HashAlgo::Sha512,
_ => return None, };
Some(ChecksumEntry {
algo,
filename: filename.to_string(),
digest: digest.to_lowercase(),
})
}
fn parse_colon_format(line: &str) -> Option<ChecksumEntry> {
let parts: Vec<&str> = line.splitn(2, ':').collect();
if parts.len() != 2 {
return None;
}
let filename = parts[0].trim();
let digest = parts[1].trim();
if digest.is_empty() || filename.is_empty() {
return None;
}
let algo = match digest.len() {
64 => HashAlgo::Sha256,
128 => HashAlgo::Sha512,
_ => return None,
};
Some(ChecksumEntry {
algo,
filename: filename.to_string(),
digest: digest.to_lowercase(),
})
}
fn parse_bare_hash(line: &str) -> Option<ChecksumEntry> {
let digest = line.trim();
if digest.is_empty() {
return None;
}
let algo = match digest.len() {
64 => HashAlgo::Sha256,
128 => HashAlgo::Sha512,
_ => return None, };
Some(ChecksumEntry {
algo,
filename: String::new(),
digest: digest.to_lowercase(),
})
}
fn verify_checksum(asset_path: &Path, checksum: &ChecksumEntry) -> Result<bool> {
use std::io::{BufReader, Read};
if !asset_path.exists() {
return Err(anyhow!(
"Asset file does not exist: {}",
asset_path.display()
));
}
let file = fs::File::open(asset_path)?;
let mut reader = BufReader::new(file);
let mut buffer = [0u8; 8192];
let computed_digest = match checksum.algo {
HashAlgo::Sha256 => {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
loop {
let n = reader.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
format!("{:x}", hasher.finalize())
}
HashAlgo::Sha512 => {
use sha2::Digest;
let mut hasher = sha2::Sha512::new();
loop {
let n = reader.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
format!("{:x}", hasher.finalize())
}
};
Ok(computed_digest.to_lowercase() == checksum.digest.to_lowercase())
}
}