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
89pub 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
106pub 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 let expected = "30e6763447e74bd7e0e7d033cd597417f29505be5724520e7d51ab9c3e03b433";
136 let checksum = Checksum::sha256(expected);
137
138 assert!(verify_file(path, &checksum).await.is_ok());
139 }
140}