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};
9use strum::{Display, EnumString, VariantArray, VariantNames};
10use winnow::{
11 ModalResult,
12 Parser,
13 combinator::{alt, eof, repeat, terminated},
14 error::{StrContext, StrContextValue},
15 token::one_of,
16};
17
18use crate::{
19 Error,
20 digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
21};
22
23pub type Blake2b512Checksum = Checksum<Blake2b512>;
27
28pub type Md5Checksum = Checksum<Md5>;
30
31pub type Sha1Checksum = Checksum<Sha1>;
33
34pub type Sha224Checksum = Checksum<Sha224>;
36
37pub type Sha256Checksum = Checksum<Sha256>;
39
40pub type Sha384Checksum = Checksum<Sha384>;
42
43pub type Sha512Checksum = Checksum<Sha512>;
45
46#[derive(
48 Clone,
49 Copy,
50 Debug,
51 Deserialize,
52 Display,
53 EnumString,
54 Eq,
55 Hash,
56 Ord,
57 PartialEq,
58 PartialOrd,
59 Serialize,
60 VariantNames,
61 VariantArray,
62)]
63pub enum ChecksumAlgorithm {
64 Blake2b512,
66 Md5,
68 Sha1,
70 Sha224,
72 Sha256,
74 Sha384,
76 Sha512,
78}
79
80impl ChecksumAlgorithm {
81 pub fn is_deprecated(&self) -> bool {
106 match self {
107 ChecksumAlgorithm::Md5 | ChecksumAlgorithm::Sha1 => true,
108 ChecksumAlgorithm::Blake2b512
109 | ChecksumAlgorithm::Sha224
110 | ChecksumAlgorithm::Sha256
111 | ChecksumAlgorithm::Sha384
112 | ChecksumAlgorithm::Sha512 => false,
113 }
114 }
115
116 pub fn non_deprecated_checksums(&self) -> Vec<ChecksumAlgorithm> {
118 <ChecksumAlgorithm as VariantArray>::VARIANTS
119 .iter()
120 .filter(|algo| !algo.is_deprecated())
121 .copied()
122 .collect::<Vec<ChecksumAlgorithm>>()
123 }
124}
125
126#[derive(Clone)]
199pub struct Checksum<D: Digest> {
200 digest: Vec<u8>,
201 _marker: PhantomData<D>,
202}
203
204impl<D: Digest> Serialize for Checksum<D> {
205 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210 where
211 S: Serializer,
212 {
213 serializer.serialize_str(&self.to_string())
214 }
215}
216
217impl<'de, D: Digest> Deserialize<'de> for Checksum<D> {
218 fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
219 where
220 De: Deserializer<'de>,
221 {
222 let s = String::deserialize(deserializer)?;
223 Checksum::from_str(&s).map_err(serde::de::Error::custom)
224 }
225}
226
227impl<D: Digest> Checksum<D> {
228 pub fn calculate_from(input: impl AsRef<[u8]>) -> Self {
240 let mut hasher = D::new();
241 hasher.update(input);
242
243 Checksum {
244 digest: hasher.finalize()[..].to_vec(),
245 _marker: PhantomData,
246 }
247 }
248
249 pub fn inner(&self) -> &[u8] {
251 &self.digest
252 }
253
254 pub fn parser(input: &mut &str) -> ModalResult<Self> {
264 #[inline]
268 fn hex_digit(input: &mut &str) -> ModalResult<u8> {
269 one_of(('0'..='9', 'a'..='f', 'A'..='F'))
270 .map(|d: char|
271 d.to_digit(16).unwrap().try_into().unwrap())
275 .context(StrContext::Expected(StrContextValue::Description(
276 "ASCII hex digit",
277 )))
278 .parse_next(input)
279 }
280
281 let hex_pair = (hex_digit, hex_digit).map(|(first, second)|
282 (first << 4) + second);
284
285 Ok(Self {
286 digest: terminated(
287 repeat(
288 <D as Digest>::output_size(),
290 hex_pair,
291 )
292 .context(StrContext::Label("hash digest")),
293 eof.context(StrContext::Expected(StrContextValue::Description(
294 "end of checksum",
295 ))),
296 )
297 .parse_next(input)?,
298 _marker: PhantomData,
299 })
300 }
301}
302
303impl<D: Digest> FromStr for Checksum<D> {
304 type Err = Error;
305 fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
324 Ok(Checksum::parser.parse(s)?)
325 }
326}
327
328impl<D: Digest> Display for Checksum<D> {
329 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
330 write!(
331 fmt,
332 "{}",
333 self.digest
334 .iter()
335 .map(|x| format!("{x:02x?}"))
336 .collect::<Vec<String>>()
337 .join("")
338 )
339 }
340}
341
342impl<D: Digest> Debug for Checksum<D> {
345 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
346 Display::fmt(&self, f)
347 }
348}
349
350impl<D: Digest> PartialEq for Checksum<D> {
351 fn eq(&self, other: &Self) -> bool {
352 self.digest == other.digest
353 }
354}
355
356impl<D: Digest> Eq for Checksum<D> {}
357
358impl<D: Digest> Ord for Checksum<D> {
359 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
360 self.digest.cmp(&other.digest)
361 }
362}
363
364impl<D: Digest> PartialOrd for Checksum<D> {
365 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
366 Some(self.cmp(other))
367 }
368}
369
370#[derive(Clone, Debug, Deserialize, Serialize)]
375#[serde(tag = "type")]
376pub enum SkippableChecksum<D: Digest + Clone> {
377 Skip,
379 #[serde(bound = "D: Digest + Clone")]
381 Checksum {
382 digest: Checksum<D>,
384 },
385}
386
387impl<D: Digest + Clone> SkippableChecksum<D> {
388 pub fn is_skipped(&self) -> bool {
392 matches!(self, SkippableChecksum::Skip)
393 }
394
395 pub fn parser(input: &mut &str) -> ModalResult<Self> {
405 terminated(
406 alt((
407 "SKIP".value(Self::Skip),
408 Checksum::parser.map(|digest| Self::Checksum { digest }),
409 )),
410 eof.context(StrContext::Expected(StrContextValue::Description(
411 "end of checksum",
412 ))),
413 )
414 .parse_next(input)
415 }
416}
417
418impl<D: Digest + Clone> FromStr for SkippableChecksum<D> {
419 type Err = Error;
420 fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
441 Ok(Self::parser.parse(s)?)
442 }
443}
444
445impl<D: Digest + Clone> Display for SkippableChecksum<D> {
446 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
447 let output = match self {
448 SkippableChecksum::Skip => "SKIP".to_string(),
449 SkippableChecksum::Checksum { digest } => digest.to_string(),
450 };
451 write!(fmt, "{output}",)
452 }
453}
454
455impl<D: Digest + Clone> PartialEq for SkippableChecksum<D> {
456 fn eq(&self, other: &Self) -> bool {
457 match (self, other) {
458 (SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
459 (SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
460 (SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
461 (
462 SkippableChecksum::Checksum { digest },
463 SkippableChecksum::Checksum {
464 digest: digest_other,
465 },
466 ) => digest == digest_other,
467 }
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use proptest::prelude::*;
474 use rstest::rstest;
475
476 use super::*;
477
478 proptest! {
479 #![proptest_config(ProptestConfig::with_cases(1000))]
480
481 #[test]
482 fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
483 prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
484 }
485
486 #[test]
487 fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
488 assert!(Blake2b512Checksum::from_str(&string).is_err());
489 }
490
491 #[test]
492 fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
493 assert!(Blake2b512Checksum::from_str(&string).is_err());
494 }
495
496 #[test]
497 fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
498 assert!(Blake2b512Checksum::from_str(&string).is_err());
499 }
500
501 #[test]
502 fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
503 prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
504 }
505
506 #[test]
507 fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
508 assert!(Sha1Checksum::from_str(&string).is_err());
509 }
510
511 #[test]
512 fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
513 assert!(Sha1Checksum::from_str(&string).is_err());
514 }
515
516 #[test]
517 fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
518 assert!(Sha1Checksum::from_str(&string).is_err());
519 }
520
521 #[test]
522 fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
523 prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
524 }
525
526 #[test]
527 fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
528 assert!(Sha224Checksum::from_str(&string).is_err());
529 }
530
531 #[test]
532 fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
533 assert!(Sha224Checksum::from_str(&string).is_err());
534 }
535
536 #[test]
537 fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
538 assert!(Sha224Checksum::from_str(&string).is_err());
539 }
540
541 #[test]
542 fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
543 prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
544 }
545
546 #[test]
547 fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
548 assert!(Sha256Checksum::from_str(&string).is_err());
549 }
550
551 #[test]
552 fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
553 assert!(Sha256Checksum::from_str(&string).is_err());
554 }
555
556 #[test]
557 fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
558 assert!(Sha256Checksum::from_str(&string).is_err());
559 }
560
561 #[test]
562 fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
563 prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
564 }
565
566 #[test]
567 fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
568 assert!(Sha384Checksum::from_str(&string).is_err());
569 }
570
571 #[test]
572 fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
573 assert!(Sha384Checksum::from_str(&string).is_err());
574 }
575
576 #[test]
577 fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
578 assert!(Sha384Checksum::from_str(&string).is_err());
579 }
580
581 #[test]
582 fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
583 prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
584 }
585
586 #[test]
587 fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
588 assert!(Sha512Checksum::from_str(&string).is_err());
589 }
590
591 #[test]
592 fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
593 assert!(Sha512Checksum::from_str(&string).is_err());
594 }
595
596 #[test]
597 fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
598 assert!(Sha512Checksum::from_str(&string).is_err());
599 }
600 }
601
602 #[rstest]
603 fn checksum_blake2b512() {
604 let data = "foo\n";
605 let digest = vec![
606 210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
607 82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
608 192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
609 217, 171, 101, 39, 142, 119,
610 ];
611 let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
612
613 let checksum = Blake2b512Checksum::calculate_from(data);
614 assert_eq!(digest, checksum.inner());
615 assert_eq!(format!("{}", &checksum), hex_digest,);
616
617 let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
618 assert_eq!(digest, checksum.inner());
619 assert_eq!(format!("{}", &checksum), hex_digest,);
620 }
621
622 #[rstest]
623 fn checksum_sha1() {
624 let data = "foo\n";
625 let digest = vec![
626 241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
627 236, 21,
628 ];
629 let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
630
631 let checksum = Sha1Checksum::calculate_from(data);
632 assert_eq!(digest, checksum.inner());
633 assert_eq!(format!("{}", &checksum), hex_digest,);
634
635 let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
636 assert_eq!(digest, checksum.inner());
637 assert_eq!(format!("{}", &checksum), hex_digest,);
638 }
639
640 #[rstest]
641 fn checksum_sha224() {
642 let data = "foo\n";
643 let digest = vec![
644 231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
645 127, 102, 41, 189, 195, 40, 111, 157, 210,
646 ];
647 let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
648
649 let checksum = Sha224Checksum::calculate_from(data);
650 assert_eq!(digest, checksum.inner());
651 assert_eq!(format!("{}", &checksum), hex_digest,);
652
653 let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
654 assert_eq!(digest, checksum.inner());
655 assert_eq!(format!("{}", &checksum), hex_digest,);
656 }
657
658 #[rstest]
659 fn checksum_sha256() {
660 let data = "foo\n";
661 let digest = vec![
662 181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
663 82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
664 ];
665 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
666
667 let checksum = Sha256Checksum::calculate_from(data);
668 assert_eq!(digest, checksum.inner());
669 assert_eq!(format!("{}", &checksum), hex_digest,);
670
671 let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
672 assert_eq!(digest, checksum.inner());
673 assert_eq!(format!("{}", &checksum), hex_digest,);
674 }
675
676 #[rstest]
677 fn checksum_sha384() {
678 let data = "foo\n";
679 let digest = vec![
680 142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
681 6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
682 232, 247, 152, 40, 155, 26, 13, 30, 6,
683 ];
684 let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
685
686 let checksum = Sha384Checksum::calculate_from(data);
687 assert_eq!(digest, checksum.inner());
688 assert_eq!(format!("{}", &checksum), hex_digest,);
689
690 let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
691 assert_eq!(digest, checksum.inner());
692 assert_eq!(format!("{}", &checksum), hex_digest,);
693 }
694
695 #[rstest]
696 fn checksum_sha512() {
697 let data = "foo\n";
698 let digest = vec![
699 12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
700 114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
701 25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
702 138, 81, 182, 232, 180, 230, 182,
703 ];
704 let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
705
706 let checksum = Sha512Checksum::calculate_from(data);
707 assert_eq!(digest, checksum.inner());
708 assert_eq!(format!("{}", &checksum), hex_digest);
709
710 let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
711 assert_eq!(digest, checksum.inner());
712 assert_eq!(format!("{}", &checksum), hex_digest);
713 }
714
715 #[rstest]
716 #[case::non_hex_digits(
717 "0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6",
718 "expected ASCII hex digit"
719 )]
720 #[case::incomplete_pair(" b ", "expected ASCII hex digit")]
721 #[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca", "expected ASCII hex digit")]
722 #[case::whitespace(
723 "d2 02 d7 95 1d f2 c4 b7 11 ca 44 b4 bc c9 d7 b3 63 fa 42 52 12 7e 05 8c 1a 91 0e c0 5b 6c d0 38 d7 1c c2 12 21 c0 31 c0 35 9f 99 3e 74 6b 07 f5 96 5c f8 c5 c3 74 6a 58 33 7a d9 ab 65 27 8e 77",
724 "expected ASCII hex digit"
725 )]
726 fn checksum_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
727 let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
728 panic!("'{input}' did not fail to parse as expected")
729 };
730 assert!(
731 err_msg.contains(err_snippet),
732 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
733 );
734 }
735
736 #[rstest]
737 fn skippable_checksum_sha256() {
738 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
739 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
740 assert_eq!(format!("{}", &checksum), hex_digest);
741 }
742
743 #[rstest]
744 fn skippable_checksum_skip() {
745 let hex_digest = "SKIP";
746 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
747
748 assert_eq!(SkippableChecksum::Skip, checksum);
749 assert_eq!(format!("{}", &checksum), hex_digest);
750 }
751}