loadsmith_core/
checksum.rs1use std::{
2 fmt::Display,
3 fs::File,
4 io::{BufReader, Read, Write},
5 path::Path,
6 str::FromStr,
7};
8
9use serde::{Deserialize, Serialize};
10use sha2::Digest;
11use walkdir::WalkDir;
12
13use crate::{Error, Result};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(into = "String", try_from = "String")]
36pub enum Checksum {
37 Blake3(blake3::Hash),
38 Sha256([u8; 32]),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
43#[serde(rename_all = "lowercase")]
44pub enum ChecksumAlgorithm {
45 Blake3,
46 Sha256,
47}
48
49impl Checksum {
50 pub fn blake3(hash: blake3::Hash) -> Self {
52 Self::from(hash)
53 }
54
55 pub fn sha256(hash: [u8; 32]) -> Self {
57 Self::Sha256(hash)
58 }
59
60 pub fn compute<R>(mut reader: R, algorithm: ChecksumAlgorithm) -> std::io::Result<Self>
70 where
71 R: Read,
72 {
73 match algorithm {
74 ChecksumAlgorithm::Blake3 => {
75 let mut hasher = blake3::Hasher::new();
76 std::io::copy(&mut reader, &mut hasher)?;
77
78 Ok(Checksum::Blake3(hasher.finalize()))
79 }
80 ChecksumAlgorithm::Sha256 => {
81 let mut hasher = digest_io::IoWrapper(sha2::Sha256::new());
82 std::io::copy(&mut reader, &mut hasher)?;
83
84 let array = hasher.0.finalize().into();
85
86 Ok(Checksum::Sha256(array))
87 }
88 }
89 }
90
91 pub fn compute_from_path(
92 path: impl AsRef<Path>,
93 algorithm: ChecksumAlgorithm,
94 ) -> std::io::Result<Self> {
95 fn hash_dir<T: Write>(mut hasher: T, path: &Path) -> std::io::Result<T> {
96 WalkDir::new(path)
97 .sort_by_file_name()
98 .into_iter()
99 .filter_map(|entry| entry.ok())
100 .try_for_each(|entry| {
101 let file_name = entry.file_name().as_encoded_bytes();
102 hasher.write_all(file_name)?;
103
104 if entry.file_type().is_file() {
105 let mut file = File::open(entry.path()).map(BufReader::new)?;
106 std::io::copy(&mut file, &mut hasher)?;
107 }
108
109 Ok::<(), std::io::Error>(())
110 })?;
111
112 Ok(hasher)
113 }
114
115 let path = path.as_ref();
116 if path.is_dir() {
117 match algorithm {
118 ChecksumAlgorithm::Blake3 => {
119 let hasher = blake3::Hasher::new();
120
121 let hasher = hash_dir(hasher, path.as_ref())?;
122
123 Ok(Checksum::Blake3(hasher.finalize()))
124 }
125 ChecksumAlgorithm::Sha256 => {
126 let hasher = digest_io::IoWrapper(sha2::Sha256::new());
127
128 let hasher = hash_dir(hasher, path.as_ref())?;
129
130 let array = hasher.0.finalize().into();
131 Ok(Checksum::Sha256(array))
132 }
133 }
134 } else {
135 let file = File::open(path).map(BufReader::new)?;
136 Self::compute(file, algorithm)
137 }
138 }
139
140 pub fn algorithm(&self) -> ChecksumAlgorithm {
142 match self {
143 Checksum::Blake3(_) => ChecksumAlgorithm::Blake3,
144 Checksum::Sha256(_) => ChecksumAlgorithm::Sha256,
145 }
146 }
147
148 pub fn without_algorithm(&self) -> WithoutAlgorithm<'_> {
150 WithoutAlgorithm(self)
151 }
152
153 pub fn from_value_str(value: &str, algorithm: ChecksumAlgorithm) -> Result<Self> {
167 match algorithm {
168 ChecksumAlgorithm::Blake3 => {
169 let hash = blake3::Hash::from_hex(value).map_err(Error::InvalidBlake3Hex)?;
170 Ok(Checksum::Blake3(hash))
171 }
172 ChecksumAlgorithm::Sha256 => {
173 let mut hash = [0u8; 32];
174 hex::decode_to_slice(value, &mut hash).map_err(Error::InvalidSha256Hex)?;
175 Ok(Checksum::Sha256(hash))
176 }
177 }
178 }
179}
180
181impl From<blake3::Hash> for Checksum {
182 fn from(hash: blake3::Hash) -> Self {
183 Checksum::Blake3(hash)
184 }
185}
186
187impl Display for Checksum {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 write!(f, "{}:{}", self.algorithm(), self.without_algorithm())
190 }
191}
192
193impl FromStr for Checksum {
194 type Err = Error;
195
196 fn from_str(s: &str) -> Result<Self> {
197 let (algorithm, value) = s.split_once(':').ok_or(Error::InvalidChecksumFormat)?;
198 Checksum::from_value_str(value, algorithm.parse()?)
199 }
200}
201
202impl From<Checksum> for String {
203 fn from(checksum: Checksum) -> Self {
204 checksum.to_string()
205 }
206}
207
208impl TryFrom<String> for Checksum {
209 type Error = Error;
210
211 fn try_from(s: String) -> Result<Self> {
212 s.parse()
213 }
214}
215
216impl Display for ChecksumAlgorithm {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 let algorithm = match self {
219 ChecksumAlgorithm::Blake3 => "blake3",
220 ChecksumAlgorithm::Sha256 => "sha256",
221 };
222
223 write!(f, "{algorithm}")
224 }
225}
226
227impl FromStr for ChecksumAlgorithm {
228 type Err = Error;
229
230 fn from_str(s: &str) -> Result<Self> {
231 match s {
232 "blake3" => Ok(ChecksumAlgorithm::Blake3),
233 "sha256" => Ok(ChecksumAlgorithm::Sha256),
234 algo => Err(Error::UnknownAlgorithm(algo.to_string())),
235 }
236 }
237}
238
239pub struct WithoutAlgorithm<'a>(&'a Checksum);
243
244impl Display for WithoutAlgorithm<'_> {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 match self.0 {
247 Checksum::Blake3(hash) => write!(f, "{}", hash.to_hex()),
248 Checksum::Sha256(hash) => write!(f, "{}", hex::encode(hash)),
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 const BLAKE3_HELLO_WORLD: &str =
259 "ede5c0b10f2ec4979c69b52f61e42ff5b413519ce09be0f14d098dcfe5f6f98d";
260
261 #[test]
262 fn checksum_display() {
263 let hash = blake3::hash(b"Hello, world!");
264 let checksum = Checksum::Blake3(hash);
265 assert_eq!(checksum.to_string(), format!("blake3:{BLAKE3_HELLO_WORLD}"));
266 }
267
268 #[test]
269 fn checksum_from_str() {
270 let checksum_str = format!("blake3:{BLAKE3_HELLO_WORLD}");
271 let checksum = Checksum::from_str(&checksum_str).unwrap();
272
273 let hash = blake3::hash(b"Hello, world!");
274 assert_eq!(checksum, Checksum::Blake3(hash));
275 }
276
277 #[test]
278 fn checksum_from_str_invalid_algorithm() {
279 let checksum_str = "unknown:abcdef";
280 let result = Checksum::from_str(&checksum_str);
281 assert!(result.is_err());
282 }
283
284 #[test]
285 fn checksum_from_str_invalid_format() {
286 let checksum_str = "blake3abcdef";
287 let result = Checksum::from_str(&checksum_str);
288 assert!(result.is_err());
289 }
290
291 #[test]
292 fn checksum_serde_json() {
293 let hash = blake3::hash(b"Hello, world!");
294 let checksum = Checksum::Blake3(hash);
295
296 let json = serde_json::to_string(&checksum).unwrap();
297 assert_eq!(json, format!("\"blake3:{BLAKE3_HELLO_WORLD}\""));
298
299 let deserialized: Checksum = serde_json::from_str(&json).unwrap();
300 assert_eq!(deserialized, checksum);
301 }
302}