fetchr-integrity 0.1.0

Streaming checksum verification (SHA256, SHA512, MD5) for Fetchr.
Documentation
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),
        }
    }
}

/// Computes the checksum of a file on disk asynchronously.
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())
}

/// Verifies that a file matching the specified expected checksum.
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();

        // SHA256 of "Hello Fetchr Integrity"
        let expected = "30e6763447e74bd7e0e7d033cd597417f29505be5724520e7d51ab9c3e03b433";
        let checksum = Checksum::sha256(expected);

        assert!(verify_file(path, &checksum).await.is_ok());
    }
}