1use crate::error::KgetError;
8use sha2::Digest as _;
9use std::fs::File;
10use std::io::Read;
11use std::path::Path;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ChecksumAlgorithm {
18 Sha256,
19 Sha512,
20 Sha1,
21 Md5,
22 Blake3,
23}
24
25impl ChecksumAlgorithm {
26 pub fn from_hex_len(hex: &str) -> Option<Self> {
33 match hex.trim().len() {
34 32 => Some(ChecksumAlgorithm::Md5),
35 40 => Some(ChecksumAlgorithm::Sha1),
36 64 => Some(ChecksumAlgorithm::Sha256),
37 128 => Some(ChecksumAlgorithm::Sha512),
38 _ => None,
39 }
40 }
41
42 pub fn name(&self) -> &'static str {
44 match self {
45 ChecksumAlgorithm::Sha256 => "sha256",
46 ChecksumAlgorithm::Sha512 => "sha512",
47 ChecksumAlgorithm::Sha1 => "sha1",
48 ChecksumAlgorithm::Md5 => "md5",
49 ChecksumAlgorithm::Blake3 => "blake3",
50 }
51 }
52}
53
54const BUF_SIZE: usize = 1024 * 1024; pub fn compute_checksum(path: &Path, algorithm: &ChecksumAlgorithm) -> Result<String, KgetError> {
62 let mut file = File::open(path)?;
63 let mut buf = vec![0u8; BUF_SIZE];
64
65 match algorithm {
66 ChecksumAlgorithm::Sha256 => {
67 let mut h = sha2::Sha256::new();
68 loop {
69 let n = file.read(&mut buf)?;
70 if n == 0 { break; }
71 sha2::Digest::update(&mut h, &buf[..n]);
72 }
73 Ok(hex::encode(sha2::Digest::finalize(h)))
74 }
75 ChecksumAlgorithm::Sha512 => {
76 let mut h = sha2::Sha512::new();
77 loop {
78 let n = file.read(&mut buf)?;
79 if n == 0 { break; }
80 sha2::Digest::update(&mut h, &buf[..n]);
81 }
82 Ok(hex::encode(sha2::Digest::finalize(h)))
83 }
84 ChecksumAlgorithm::Sha1 => {
85 let mut h = sha1::Sha1::new();
86 loop {
87 let n = file.read(&mut buf)?;
88 if n == 0 { break; }
89 sha1::Digest::update(&mut h, &buf[..n]);
90 }
91 Ok(hex::encode(sha1::Digest::finalize(h)))
92 }
93 ChecksumAlgorithm::Md5 => {
94 let mut h = md5::Md5::new();
95 loop {
96 let n = file.read(&mut buf)?;
97 if n == 0 { break; }
98 md5::Digest::update(&mut h, &buf[..n]);
99 }
100 Ok(hex::encode(md5::Digest::finalize(h)))
101 }
102 ChecksumAlgorithm::Blake3 => {
103 let mut h = blake3::Hasher::new();
104 loop {
105 let n = file.read(&mut buf)?;
106 if n == 0 { break; }
107 h.update(&buf[..n]);
108 }
109 Ok(h.finalize().to_hex().to_string())
110 }
111 }
112}
113
114pub fn parse_sidecar(content: &str, filename: &str) -> Option<(ChecksumAlgorithm, String)> {
126 for line in content.lines() {
127 let line = line.trim();
128 if line.is_empty() || line.starts_with('#') { continue; }
129
130 let gnu = line.split_once(" ")
133 .or_else(|| line.split_once(" *"));
134
135 if let Some((hash_part, file_part)) = gnu {
136 let hash = hash_part.trim();
137 let file = file_part.trim().trim_start_matches('*').trim();
138 if matches_filename(file, filename) {
139 if let Some(algo) = ChecksumAlgorithm::from_hex_len(hash) {
140 return Some((algo, hash.to_lowercase()));
141 }
142 }
143 }
144
145 if let Some(eq_pos) = line.find(" = ") {
147 let hash = line[eq_pos + 3..].trim().to_lowercase();
148 let prefix = &line[..eq_pos];
149 if let (Some(lp), Some(rp)) = (prefix.find('('), prefix.rfind(')')) {
150 let file = prefix[lp + 1..rp].trim();
151 if matches_filename(file, filename) {
152 let algo_str = prefix[..lp].trim().to_uppercase();
153 let algo = match algo_str.as_str() {
154 "SHA256" => Some(ChecksumAlgorithm::Sha256),
155 "SHA512" => Some(ChecksumAlgorithm::Sha512),
156 "SHA1" => Some(ChecksumAlgorithm::Sha1),
157 "MD5" => Some(ChecksumAlgorithm::Md5),
158 "BLAKE3" => Some(ChecksumAlgorithm::Blake3),
159 _ => None,
160 };
161 if let Some(algo) = algo {
162 return Some((algo, hash));
163 }
164 }
165 }
166 }
167 }
168 None
169}
170
171fn matches_filename(candidate: &str, target: &str) -> bool {
173 let c = candidate.trim_start_matches("./");
174 let t = target.trim_start_matches("./");
175 c == t
176 || c.ends_with(&format!("/{t}"))
177 || t.ends_with(&format!("/{c}"))
178 || std::path::Path::new(c).file_name() == std::path::Path::new(t).file_name()
179}
180
181#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn parse_gnu_format() {
189 let sidecar = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 empty.bin\n\
190 abc123def456abc123def456abc123def456abc123def456abc123def456abc1 file.iso\n";
191 let (algo, hash) = parse_sidecar(sidecar, "file.iso").unwrap();
192 assert_eq!(algo, ChecksumAlgorithm::Sha256);
193 assert_eq!(hash, "abc123def456abc123def456abc123def456abc123def456abc123def456abc1");
194 }
195
196 #[test]
197 fn parse_bsd_format() {
198 let sidecar = "SHA256 (ubuntu.iso) = deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n";
199 let (algo, hash) = parse_sidecar(sidecar, "ubuntu.iso").unwrap();
200 assert_eq!(algo, ChecksumAlgorithm::Sha256);
201 assert_eq!(hash, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
202 }
203
204 #[test]
205 fn parse_md5_by_length() {
206 let sidecar = "d41d8cd98f00b204e9800998ecf8427e empty.bin\n";
207 let (algo, _) = parse_sidecar(sidecar, "empty.bin").unwrap();
208 assert_eq!(algo, ChecksumAlgorithm::Md5);
209 }
210
211 #[test]
212 fn algo_from_hex_len() {
213 assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(32)), Some(ChecksumAlgorithm::Md5));
214 assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(40)), Some(ChecksumAlgorithm::Sha1));
215 assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(64)), Some(ChecksumAlgorithm::Sha256));
216 assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(128)), Some(ChecksumAlgorithm::Sha512));
217 assert_eq!(ChecksumAlgorithm::from_hex_len(&"a".repeat(10)), None);
218 }
219}