use crate::error::KgetError;
use sha2::Digest as _;
use std::fs::File;
use std::io::Read;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChecksumAlgorithm {
Sha256,
Sha512,
Sha1,
Md5,
Blake3,
}
impl ChecksumAlgorithm {
pub fn from_hex_len(hex: &str) -> Option<Self> {
match hex.trim().len() {
32 => Some(ChecksumAlgorithm::Md5),
40 => Some(ChecksumAlgorithm::Sha1),
64 => Some(ChecksumAlgorithm::Sha256),
128 => Some(ChecksumAlgorithm::Sha512),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
ChecksumAlgorithm::Sha256 => "sha256",
ChecksumAlgorithm::Sha512 => "sha512",
ChecksumAlgorithm::Sha1 => "sha1",
ChecksumAlgorithm::Md5 => "md5",
ChecksumAlgorithm::Blake3 => "blake3",
}
}
}
const BUF_SIZE: usize = 1024 * 1024;
pub fn compute_checksum(path: &Path, algorithm: &ChecksumAlgorithm) -> Result<String, KgetError> {
let mut file = File::open(path)?;
let mut buf = vec![0u8; BUF_SIZE];
match algorithm {
ChecksumAlgorithm::Sha256 => {
let mut h = sha2::Sha256::new();
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
sha2::Digest::update(&mut h, &buf[..n]);
}
Ok(hex::encode(sha2::Digest::finalize(h)))
}
ChecksumAlgorithm::Sha512 => {
let mut h = sha2::Sha512::new();
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
sha2::Digest::update(&mut h, &buf[..n]);
}
Ok(hex::encode(sha2::Digest::finalize(h)))
}
ChecksumAlgorithm::Sha1 => {
let mut h = sha1::Sha1::new();
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
sha1::Digest::update(&mut h, &buf[..n]);
}
Ok(hex::encode(sha1::Digest::finalize(h)))
}
ChecksumAlgorithm::Md5 => {
let mut h = md5::Md5::new();
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
md5::Digest::update(&mut h, &buf[..n]);
}
Ok(hex::encode(md5::Digest::finalize(h)))
}
ChecksumAlgorithm::Blake3 => {
let mut h = blake3::Hasher::new();
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
h.update(&buf[..n]);
}
Ok(h.finalize().to_hex().to_string())
}
}
}
pub fn parse_sidecar(content: &str, filename: &str) -> Option<(ChecksumAlgorithm, String)> {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') { continue; }
let gnu = line.split_once(" ")
.or_else(|| line.split_once(" *"));
if let Some((hash_part, file_part)) = gnu {
let hash = hash_part.trim();
let file = file_part.trim().trim_start_matches('*').trim();
if matches_filename(file, filename) {
if let Some(algo) = ChecksumAlgorithm::from_hex_len(hash) {
return Some((algo, hash.to_lowercase()));
}
}
}
if let Some(eq_pos) = line.find(" = ") {
let hash = line[eq_pos + 3..].trim().to_lowercase();
let prefix = &line[..eq_pos];
if let (Some(lp), Some(rp)) = (prefix.find('('), prefix.rfind(')')) {
let file = prefix[lp + 1..rp].trim();
if matches_filename(file, filename) {
let algo_str = prefix[..lp].trim().to_uppercase();
let algo = match algo_str.as_str() {
"SHA256" => Some(ChecksumAlgorithm::Sha256),
"SHA512" => Some(ChecksumAlgorithm::Sha512),
"SHA1" => Some(ChecksumAlgorithm::Sha1),
"MD5" => Some(ChecksumAlgorithm::Md5),
"BLAKE3" => Some(ChecksumAlgorithm::Blake3),
_ => None,
};
if let Some(algo) = algo {
return Some((algo, hash));
}
}
}
}
}
None
}
fn matches_filename(candidate: &str, target: &str) -> bool {
let c = candidate.trim_start_matches("./");
let t = target.trim_start_matches("./");
c == t
|| c.ends_with(&format!("/{t}"))
|| t.ends_with(&format!("/{c}"))
|| std::path::Path::new(c).file_name() == std::path::Path::new(t).file_name()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_gnu_format() {
let sidecar = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 empty.bin\n\
abc123def456abc123def456abc123def456abc123def456abc123def456abc1 file.iso\n";
let (algo, hash) = parse_sidecar(sidecar, "file.iso").unwrap();
assert_eq!(algo, ChecksumAlgorithm::Sha256);
assert_eq!(hash, "abc123def456abc123def456abc123def456abc123def456abc123def456abc1");
}
#[test]
fn parse_bsd_format() {
let sidecar = "SHA256 (ubuntu.iso) = deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n";
let (algo, hash) = parse_sidecar(sidecar, "ubuntu.iso").unwrap();
assert_eq!(algo, ChecksumAlgorithm::Sha256);
assert_eq!(hash, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
}
#[test]
fn parse_md5_by_length() {
let sidecar = "d41d8cd98f00b204e9800998ecf8427e empty.bin\n";
let (algo, _) = parse_sidecar(sidecar, "empty.bin").unwrap();
assert_eq!(algo, ChecksumAlgorithm::Md5);
}
#[test]
fn algo_from_hex_len() {
assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(32)), Some(ChecksumAlgorithm::Md5));
assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(40)), Some(ChecksumAlgorithm::Sha1));
assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(64)), Some(ChecksumAlgorithm::Sha256));
assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(128)), Some(ChecksumAlgorithm::Sha512));
assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(10)), None);
}
}