Skip to main content

fetchr_integrity/
lib.rs

1use std::path::Path;
2use md5::Context as Md5Context;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256, Sha512};
5use thiserror::Error;
6use tokio::fs::File;
7use tokio::io::AsyncReadExt;
8
9#[derive(Error, Debug)]
10pub enum IntegrityError {
11    #[error("I/O error during checksum calculation: {0}")]
12    Io(#[from] std::io::Error),
13
14    #[error("Checksum mismatch! Expected: {expected}, Computed: {computed}")]
15    Mismatch { expected: String, computed: String },
16
17    #[error("Invalid hex checksum: {0}")]
18    InvalidHex(String),
19}
20
21pub type Result<T> = std::result::Result<T, IntegrityError>;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum Algorithm {
25    Sha256,
26    Sha512,
27    Md5,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct Checksum {
32    pub algorithm: Algorithm,
33    pub expected_hex: String,
34}
35
36impl Checksum {
37    pub fn new(algorithm: Algorithm, expected_hex: impl Into<String>) -> Self {
38        Self {
39            algorithm,
40            expected_hex: expected_hex.into().trim().to_lowercase(),
41        }
42    }
43
44    pub fn sha256(hex: impl Into<String>) -> Self {
45        Self::new(Algorithm::Sha256, hex)
46    }
47
48    pub fn sha512(hex: impl Into<String>) -> Self {
49        Self::new(Algorithm::Sha512, hex)
50    }
51
52    pub fn md5(hex: impl Into<String>) -> Self {
53        Self::new(Algorithm::Md5, hex)
54    }
55}
56
57pub enum Hasher {
58    Sha256(Sha256),
59    Sha512(Sha512),
60    Md5(Md5Context),
61}
62
63impl Hasher {
64    pub fn new(algo: Algorithm) -> Self {
65        match algo {
66            Algorithm::Sha256 => Hasher::Sha256(Sha256::new()),
67            Algorithm::Sha512 => Hasher::Sha512(Sha512::new()),
68            Algorithm::Md5 => Hasher::Md5(Md5Context::new()),
69        }
70    }
71
72    pub fn update(&mut self, bytes: &[u8]) {
73        match self {
74            Hasher::Sha256(h) => h.update(bytes),
75            Hasher::Sha512(h) => h.update(bytes),
76            Hasher::Md5(h) => h.consume(bytes),
77        }
78    }
79
80    pub fn finalize(self) -> String {
81        match self {
82            Hasher::Sha256(h) => hex::encode(h.finalize()),
83            Hasher::Sha512(h) => hex::encode(h.finalize()),
84            Hasher::Md5(h) => hex::encode(h.compute().0),
85        }
86    }
87}
88
89/// Computes the checksum of a file on disk asynchronously.
90pub async fn compute_checksum(path: &Path, algo: Algorithm) -> Result<String> {
91    let mut file = File::open(path).await?;
92    let mut hasher = Hasher::new(algo);
93    let mut buffer = vec![0u8; 64 * 1024];
94
95    loop {
96        let n = file.read(&mut buffer).await?;
97        if n == 0 {
98            break;
99        }
100        hasher.update(&buffer[..n]);
101    }
102
103    Ok(hasher.finalize())
104}
105
106/// Verifies that a file matching the specified expected checksum.
107pub async fn verify_file(path: &Path, expected: &Checksum) -> Result<()> {
108    let computed = compute_checksum(path, expected.algorithm).await?;
109    let expected_clean = expected.expected_hex.trim().to_lowercase();
110
111    if computed == expected_clean {
112        Ok(())
113    } else {
114        Err(IntegrityError::Mismatch {
115            expected: expected_clean,
116            computed,
117        })
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use std::io::Write;
125    use tempfile::NamedTempFile;
126
127    #[tokio::test]
128    async fn test_sha256_verification() {
129        let mut tmp = NamedTempFile::new().unwrap();
130        tmp.write_all(b"Hello Fetchr Integrity").unwrap();
131        tmp.flush().unwrap();
132        let path = tmp.path();
133
134        // SHA256 of "Hello Fetchr Integrity"
135        let expected = "30e6763447e74bd7e0e7d033cd597417f29505be5724520e7d51ab9c3e03b433";
136        let checksum = Checksum::sha256(expected);
137
138        assert!(verify_file(path, &checksum).await.is_ok());
139    }
140}