1use std::{
2 fmt::{Debug, Display, Formatter},
3 marker::PhantomData,
4 str::FromStr,
5};
6
7pub use digest::Digest;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use crate::{
11 Error,
12 digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
13};
14
15pub type Blake2b512Checksum = Checksum<Blake2b512>;
19
20pub type Md5Checksum = Checksum<Md5>;
22
23pub type Sha1Checksum = Checksum<Sha1>;
25
26pub type Sha224Checksum = Checksum<Sha224>;
28
29pub type Sha256Checksum = Checksum<Sha256>;
31
32pub type Sha384Checksum = Checksum<Sha384>;
34
35pub type Sha512Checksum = Checksum<Sha512>;
37
38#[derive(Clone)]
111pub struct Checksum<D: Digest> {
112 digest: Vec<u8>,
113 _marker: PhantomData<D>,
114}
115
116impl<D: Digest> Serialize for Checksum<D> {
117 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122 where
123 S: Serializer,
124 {
125 serializer.serialize_str(&self.to_string())
126 }
127}
128
129impl<'de, D: Digest> Deserialize<'de> for Checksum<D> {
130 fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
131 where
132 De: Deserializer<'de>,
133 {
134 let s = String::deserialize(deserializer)?;
135 Checksum::from_str(&s).map_err(serde::de::Error::custom)
136 }
137}
138
139impl<D: Digest> Checksum<D> {
140 pub fn calculate_from(input: impl AsRef<[u8]>) -> Self {
152 let mut hasher = D::new();
153 hasher.update(input);
154
155 Checksum {
156 digest: hasher.finalize()[..].to_vec(),
157 _marker: PhantomData,
158 }
159 }
160
161 pub fn inner(&self) -> &[u8] {
163 &self.digest
164 }
165}
166
167impl<D: Digest> FromStr for Checksum<D> {
168 type Err = Error;
169 fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
187 let input = s.replace(' ', "").to_lowercase();
188 if input.len() != <D as Digest>::output_size() * 2 {
190 return Err(Error::IncorrectLength {
191 length: input.len(),
192 expected: <D as Digest>::output_size() * 2,
193 });
194 }
195
196 let mut digest = vec![];
197 for i in (0..input.len()).step_by(2) {
198 let src = &input[i..i + 2];
199 match u8::from_str_radix(src, 16) {
200 Ok(byte) => digest.push(byte),
201 Err(e) => {
202 return Err(Error::InvalidInteger {
203 kind: e.kind().clone(),
204 });
205 }
206 }
207 }
208
209 Ok(Checksum {
210 digest,
211 _marker: PhantomData,
212 })
213 }
214}
215
216impl<D: Digest> Display for Checksum<D> {
217 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
218 write!(
219 fmt,
220 "{}",
221 self.digest
222 .iter()
223 .map(|x| format!("{:02x?}", x))
224 .collect::<Vec<String>>()
225 .join("")
226 )
227 }
228}
229
230impl<D: Digest> Debug for Checksum<D> {
233 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
234 Display::fmt(&self, f)
235 }
236}
237
238impl<D: Digest> PartialEq for Checksum<D> {
239 fn eq(&self, other: &Self) -> bool {
240 self.digest == other.digest
241 }
242}
243
244#[derive(Debug, Clone, Deserialize, Serialize)]
249#[serde(tag = "type")]
250pub enum SkippableChecksum<D: Digest + Clone> {
251 Skip,
253 #[serde(bound = "D: Digest + Clone")]
255 Checksum {
256 digest: Checksum<D>,
258 },
259}
260
261impl<D: Digest + Clone> FromStr for SkippableChecksum<D> {
262 type Err = Error;
263 fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
282 if s.trim() == "SKIP" {
284 return Ok(SkippableChecksum::Skip);
285 }
286
287 let checksum = Checksum::from_str(s)?;
289
290 Ok(SkippableChecksum::Checksum { digest: checksum })
291 }
292}
293
294impl<D: Digest + Clone> Display for SkippableChecksum<D> {
295 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
296 let output = match self {
297 SkippableChecksum::Skip => "SKIP".to_string(),
298 SkippableChecksum::Checksum { digest } => digest.to_string(),
299 };
300 write!(fmt, "{output}",)
301 }
302}
303
304impl<D: Digest + Clone> PartialEq for SkippableChecksum<D> {
305 fn eq(&self, other: &Self) -> bool {
306 match (self, other) {
307 (SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
308 (SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
309 (SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
310 (
311 SkippableChecksum::Checksum { digest },
312 SkippableChecksum::Checksum {
313 digest: digest_other,
314 },
315 ) => digest == digest_other,
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use proptest::prelude::*;
323 use rstest::rstest;
324
325 use super::*;
326
327 proptest! {
328 #![proptest_config(ProptestConfig::with_cases(1000))]
329
330 #[test]
331 fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
332 prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
333 }
334
335 #[test]
336 fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
337 assert!(Blake2b512Checksum::from_str(&string).is_err());
338 }
339
340 #[test]
341 fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
342 assert!(Blake2b512Checksum::from_str(&string).is_err());
343 }
344
345 #[test]
346 fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
347 assert!(Blake2b512Checksum::from_str(&string).is_err());
348 }
349
350 #[test]
351 fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
352 prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
353 }
354
355 #[test]
356 fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
357 assert!(Sha1Checksum::from_str(&string).is_err());
358 }
359
360 #[test]
361 fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
362 assert!(Sha1Checksum::from_str(&string).is_err());
363 }
364
365 #[test]
366 fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
367 assert!(Sha1Checksum::from_str(&string).is_err());
368 }
369
370 #[test]
371 fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
372 prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
373 }
374
375 #[test]
376 fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
377 assert!(Sha224Checksum::from_str(&string).is_err());
378 }
379
380 #[test]
381 fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
382 assert!(Sha224Checksum::from_str(&string).is_err());
383 }
384
385 #[test]
386 fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
387 assert!(Sha224Checksum::from_str(&string).is_err());
388 }
389
390 #[test]
391 fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
392 prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
393 }
394
395 #[test]
396 fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
397 assert!(Sha256Checksum::from_str(&string).is_err());
398 }
399
400 #[test]
401 fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
402 assert!(Sha256Checksum::from_str(&string).is_err());
403 }
404
405 #[test]
406 fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
407 assert!(Sha256Checksum::from_str(&string).is_err());
408 }
409
410 #[test]
411 fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
412 prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
413 }
414
415 #[test]
416 fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
417 assert!(Sha384Checksum::from_str(&string).is_err());
418 }
419
420 #[test]
421 fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
422 assert!(Sha384Checksum::from_str(&string).is_err());
423 }
424
425 #[test]
426 fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
427 assert!(Sha384Checksum::from_str(&string).is_err());
428 }
429
430 #[test]
431 fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
432 prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
433 }
434
435 #[test]
436 fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
437 assert!(Sha512Checksum::from_str(&string).is_err());
438 }
439
440 #[test]
441 fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
442 assert!(Sha512Checksum::from_str(&string).is_err());
443 }
444
445 #[test]
446 fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
447 assert!(Sha512Checksum::from_str(&string).is_err());
448 }
449 }
450
451 #[rstest]
452 fn checksum_blake2b512() {
453 let data = "foo\n";
454 let digest = vec![
455 210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
456 82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
457 192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
458 217, 171, 101, 39, 142, 119,
459 ];
460 let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
461
462 let checksum = Blake2b512Checksum::calculate_from(data);
463 assert_eq!(digest, checksum.inner());
464 assert_eq!(format!("{}", &checksum), hex_digest,);
465
466 let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
467 assert_eq!(digest, checksum.inner());
468 assert_eq!(format!("{}", &checksum), hex_digest,);
469 }
470
471 #[rstest]
472 fn checksum_sha1() {
473 let data = "foo\n";
474 let digest = vec![
475 241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
476 236, 21,
477 ];
478 let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
479
480 let checksum = Sha1Checksum::calculate_from(data);
481 assert_eq!(digest, checksum.inner());
482 assert_eq!(format!("{}", &checksum), hex_digest,);
483
484 let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
485 assert_eq!(digest, checksum.inner());
486 assert_eq!(format!("{}", &checksum), hex_digest,);
487 }
488
489 #[rstest]
490 fn checksum_sha224() {
491 let data = "foo\n";
492 let digest = vec![
493 231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
494 127, 102, 41, 189, 195, 40, 111, 157, 210,
495 ];
496 let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
497
498 let checksum = Sha224Checksum::calculate_from(data);
499 assert_eq!(digest, checksum.inner());
500 assert_eq!(format!("{}", &checksum), hex_digest,);
501
502 let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
503 assert_eq!(digest, checksum.inner());
504 assert_eq!(format!("{}", &checksum), hex_digest,);
505 }
506
507 #[rstest]
508 fn checksum_sha256() {
509 let data = "foo\n";
510 let digest = vec![
511 181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
512 82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
513 ];
514 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
515
516 let checksum = Sha256Checksum::calculate_from(data);
517 assert_eq!(digest, checksum.inner());
518 assert_eq!(format!("{}", &checksum), hex_digest,);
519
520 let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
521 assert_eq!(digest, checksum.inner());
522 assert_eq!(format!("{}", &checksum), hex_digest,);
523 }
524
525 #[rstest]
526 fn checksum_sha384() {
527 let data = "foo\n";
528 let digest = vec![
529 142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
530 6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
531 232, 247, 152, 40, 155, 26, 13, 30, 6,
532 ];
533 let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
534
535 let checksum = Sha384Checksum::calculate_from(data);
536 assert_eq!(digest, checksum.inner());
537 assert_eq!(format!("{}", &checksum), hex_digest,);
538
539 let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
540 assert_eq!(digest, checksum.inner());
541 assert_eq!(format!("{}", &checksum), hex_digest,);
542 }
543
544 #[rstest]
545 fn checksum_sha512() {
546 let data = "foo\n";
547 let digest = vec![
548 12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
549 114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
550 25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
551 138, 81, 182, 232, 180, 230, 182,
552 ];
553 let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
554
555 let checksum = Sha512Checksum::calculate_from(data);
556 assert_eq!(digest, checksum.inner());
557 assert_eq!(format!("{}", &checksum), hex_digest);
558
559 let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
560 assert_eq!(digest, checksum.inner());
561 assert_eq!(format!("{}", &checksum), hex_digest);
562 }
563
564 #[rstest]
565 fn skippable_checksum_sha256() {
566 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
567 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
568 assert_eq!(format!("{}", &checksum), hex_digest);
569 }
570
571 #[rstest]
572 fn skippable_checksum_skip() {
573 let hex_digest = "SKIP";
574 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
575
576 assert_eq!(SkippableChecksum::Skip, checksum);
577 assert_eq!(format!("{}", &checksum), hex_digest);
578 }
579}