use std::path::Path;
use md5::Context as Md5Context;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256, Sha512};
use thiserror::Error;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
#[derive(Error, Debug)]
pub enum IntegrityError {
#[error("I/O error during checksum calculation: {0}")]
Io(#[from] std::io::Error),
#[error("Checksum mismatch! Expected: {expected}, Computed: {computed}")]
Mismatch { expected: String, computed: String },
#[error("Invalid hex checksum: {0}")]
InvalidHex(String),
}
pub type Result<T> = std::result::Result<T, IntegrityError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Algorithm {
Sha256,
Sha512,
Md5,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Checksum {
pub algorithm: Algorithm,
pub expected_hex: String,
}
impl Checksum {
pub fn new(algorithm: Algorithm, expected_hex: impl Into<String>) -> Self {
Self {
algorithm,
expected_hex: expected_hex.into().trim().to_lowercase(),
}
}
pub fn sha256(hex: impl Into<String>) -> Self {
Self::new(Algorithm::Sha256, hex)
}
pub fn sha512(hex: impl Into<String>) -> Self {
Self::new(Algorithm::Sha512, hex)
}
pub fn md5(hex: impl Into<String>) -> Self {
Self::new(Algorithm::Md5, hex)
}
}
pub enum Hasher {
Sha256(Sha256),
Sha512(Sha512),
Md5(Md5Context),
}
impl Hasher {
pub fn new(algo: Algorithm) -> Self {
match algo {
Algorithm::Sha256 => Hasher::Sha256(Sha256::new()),
Algorithm::Sha512 => Hasher::Sha512(Sha512::new()),
Algorithm::Md5 => Hasher::Md5(Md5Context::new()),
}
}
pub fn update(&mut self, bytes: &[u8]) {
match self {
Hasher::Sha256(h) => h.update(bytes),
Hasher::Sha512(h) => h.update(bytes),
Hasher::Md5(h) => h.consume(bytes),
}
}
pub fn finalize(self) -> String {
match self {
Hasher::Sha256(h) => hex::encode(h.finalize()),
Hasher::Sha512(h) => hex::encode(h.finalize()),
Hasher::Md5(h) => hex::encode(h.compute().0),
}
}
}
pub async fn compute_checksum(path: &Path, algo: Algorithm) -> Result<String> {
let mut file = File::open(path).await?;
let mut hasher = Hasher::new(algo);
let mut buffer = vec![0u8; 64 * 1024];
loop {
let n = file.read(&mut buffer).await?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
Ok(hasher.finalize())
}
pub async fn verify_file(path: &Path, expected: &Checksum) -> Result<()> {
let computed = compute_checksum(path, expected.algorithm).await?;
let expected_clean = expected.expected_hex.trim().to_lowercase();
if computed == expected_clean {
Ok(())
} else {
Err(IntegrityError::Mismatch {
expected: expected_clean,
computed,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_sha256_verification() {
let mut tmp = NamedTempFile::new().unwrap();
tmp.write_all(b"Hello Fetchr Integrity").unwrap();
tmp.flush().unwrap();
let path = tmp.path();
let expected = "30e6763447e74bd7e0e7d033cd597417f29505be5724520e7d51ab9c3e03b433";
let checksum = Checksum::sha256(expected);
assert!(verify_file(path, &checksum).await.is_ok());
}
}