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 winnow::{
10 ModalResult,
11 Parser,
12 combinator::{alt, eof, repeat, terminated},
13 error::{StrContext, StrContextValue},
14 token::one_of,
15};
16
17use crate::{
18 Error,
19 digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
20};
21
22pub type Blake2b512Checksum = Checksum<Blake2b512>;
26
27pub type Md5Checksum = Checksum<Md5>;
29
30pub type Sha1Checksum = Checksum<Sha1>;
32
33pub type Sha224Checksum = Checksum<Sha224>;
35
36pub type Sha256Checksum = Checksum<Sha256>;
38
39pub type Sha384Checksum = Checksum<Sha384>;
41
42pub type Sha512Checksum = Checksum<Sha512>;
44
45#[derive(Clone)]
118pub struct Checksum<D: Digest> {
119 digest: Vec<u8>,
120 _marker: PhantomData<D>,
121}
122
123impl<D: Digest> Serialize for Checksum<D> {
124 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129 where
130 S: Serializer,
131 {
132 serializer.serialize_str(&self.to_string())
133 }
134}
135
136impl<'de, D: Digest> Deserialize<'de> for Checksum<D> {
137 fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
138 where
139 De: Deserializer<'de>,
140 {
141 let s = String::deserialize(deserializer)?;
142 Checksum::from_str(&s).map_err(serde::de::Error::custom)
143 }
144}
145
146impl<D: Digest> Checksum<D> {
147 pub fn calculate_from(input: impl AsRef<[u8]>) -> Self {
159 let mut hasher = D::new();
160 hasher.update(input);
161
162 Checksum {
163 digest: hasher.finalize()[..].to_vec(),
164 _marker: PhantomData,
165 }
166 }
167
168 pub fn inner(&self) -> &[u8] {
170 &self.digest
171 }
172
173 pub fn parser(input: &mut &str) -> ModalResult<Self> {
183 #[inline]
187 fn hex_digit(input: &mut &str) -> ModalResult<u8> {
188 one_of(('0'..='9', 'a'..='f', 'A'..='F'))
189 .map(|d: char|
190 d.to_digit(16).unwrap().try_into().unwrap())
194 .context(StrContext::Expected(StrContextValue::Description(
195 "ASCII hex digit",
196 )))
197 .parse_next(input)
198 }
199
200 let hex_pair = (hex_digit, hex_digit).map(|(first, second)|
201 (first << 4) + second);
203
204 Ok(Self {
205 digest: terminated(
206 repeat(
207 <D as Digest>::output_size(),
209 hex_pair,
210 )
211 .context(StrContext::Label("hash digest")),
212 eof.context(StrContext::Expected(StrContextValue::Description(
213 "end of checksum",
214 ))),
215 )
216 .parse_next(input)?,
217 _marker: PhantomData,
218 })
219 }
220}
221
222impl<D: Digest> FromStr for Checksum<D> {
223 type Err = Error;
224 fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
243 Ok(Checksum::parser.parse(s)?)
244 }
245}
246
247impl<D: Digest> Display for Checksum<D> {
248 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
249 write!(
250 fmt,
251 "{}",
252 self.digest
253 .iter()
254 .map(|x| format!("{:02x?}", x))
255 .collect::<Vec<String>>()
256 .join("")
257 )
258 }
259}
260
261impl<D: Digest> Debug for Checksum<D> {
264 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
265 Display::fmt(&self, f)
266 }
267}
268
269impl<D: Digest> PartialEq for Checksum<D> {
270 fn eq(&self, other: &Self) -> bool {
271 self.digest == other.digest
272 }
273}
274
275impl<D: Digest> Eq for Checksum<D> {}
276
277impl<D: Digest> Ord for Checksum<D> {
278 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
279 self.digest.cmp(&other.digest)
280 }
281}
282
283impl<D: Digest> PartialOrd for Checksum<D> {
284 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
285 Some(self.cmp(other))
286 }
287}
288
289#[derive(Debug, Clone, Deserialize, Serialize)]
294#[serde(tag = "type")]
295pub enum SkippableChecksum<D: Digest + Clone> {
296 Skip,
298 #[serde(bound = "D: Digest + Clone")]
300 Checksum {
301 digest: Checksum<D>,
303 },
304}
305
306impl<D: Digest + Clone> SkippableChecksum<D> {
307 pub fn parser(input: &mut &str) -> ModalResult<Self> {
317 terminated(
318 alt((
319 "SKIP".value(Self::Skip),
320 Checksum::parser.map(|digest| Self::Checksum { digest }),
321 )),
322 eof.context(StrContext::Expected(StrContextValue::Description(
323 "end of checksum",
324 ))),
325 )
326 .parse_next(input)
327 }
328}
329
330impl<D: Digest + Clone> FromStr for SkippableChecksum<D> {
331 type Err = Error;
332 fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
353 Ok(Self::parser.parse(s)?)
354 }
355}
356
357impl<D: Digest + Clone> Display for SkippableChecksum<D> {
358 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
359 let output = match self {
360 SkippableChecksum::Skip => "SKIP".to_string(),
361 SkippableChecksum::Checksum { digest } => digest.to_string(),
362 };
363 write!(fmt, "{output}",)
364 }
365}
366
367impl<D: Digest + Clone> PartialEq for SkippableChecksum<D> {
368 fn eq(&self, other: &Self) -> bool {
369 match (self, other) {
370 (SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
371 (SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
372 (SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
373 (
374 SkippableChecksum::Checksum { digest },
375 SkippableChecksum::Checksum {
376 digest: digest_other,
377 },
378 ) => digest == digest_other,
379 }
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use proptest::prelude::*;
386 use rstest::rstest;
387
388 use super::*;
389
390 proptest! {
391 #![proptest_config(ProptestConfig::with_cases(1000))]
392
393 #[test]
394 fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
395 prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
396 }
397
398 #[test]
399 fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
400 assert!(Blake2b512Checksum::from_str(&string).is_err());
401 }
402
403 #[test]
404 fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
405 assert!(Blake2b512Checksum::from_str(&string).is_err());
406 }
407
408 #[test]
409 fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
410 assert!(Blake2b512Checksum::from_str(&string).is_err());
411 }
412
413 #[test]
414 fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
415 prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
416 }
417
418 #[test]
419 fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
420 assert!(Sha1Checksum::from_str(&string).is_err());
421 }
422
423 #[test]
424 fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
425 assert!(Sha1Checksum::from_str(&string).is_err());
426 }
427
428 #[test]
429 fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
430 assert!(Sha1Checksum::from_str(&string).is_err());
431 }
432
433 #[test]
434 fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
435 prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
436 }
437
438 #[test]
439 fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
440 assert!(Sha224Checksum::from_str(&string).is_err());
441 }
442
443 #[test]
444 fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
445 assert!(Sha224Checksum::from_str(&string).is_err());
446 }
447
448 #[test]
449 fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
450 assert!(Sha224Checksum::from_str(&string).is_err());
451 }
452
453 #[test]
454 fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
455 prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
456 }
457
458 #[test]
459 fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
460 assert!(Sha256Checksum::from_str(&string).is_err());
461 }
462
463 #[test]
464 fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
465 assert!(Sha256Checksum::from_str(&string).is_err());
466 }
467
468 #[test]
469 fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
470 assert!(Sha256Checksum::from_str(&string).is_err());
471 }
472
473 #[test]
474 fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
475 prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
476 }
477
478 #[test]
479 fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
480 assert!(Sha384Checksum::from_str(&string).is_err());
481 }
482
483 #[test]
484 fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
485 assert!(Sha384Checksum::from_str(&string).is_err());
486 }
487
488 #[test]
489 fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
490 assert!(Sha384Checksum::from_str(&string).is_err());
491 }
492
493 #[test]
494 fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
495 prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
496 }
497
498 #[test]
499 fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
500 assert!(Sha512Checksum::from_str(&string).is_err());
501 }
502
503 #[test]
504 fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
505 assert!(Sha512Checksum::from_str(&string).is_err());
506 }
507
508 #[test]
509 fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
510 assert!(Sha512Checksum::from_str(&string).is_err());
511 }
512 }
513
514 #[rstest]
515 fn checksum_blake2b512() {
516 let data = "foo\n";
517 let digest = vec![
518 210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
519 82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
520 192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
521 217, 171, 101, 39, 142, 119,
522 ];
523 let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
524
525 let checksum = Blake2b512Checksum::calculate_from(data);
526 assert_eq!(digest, checksum.inner());
527 assert_eq!(format!("{}", &checksum), hex_digest,);
528
529 let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
530 assert_eq!(digest, checksum.inner());
531 assert_eq!(format!("{}", &checksum), hex_digest,);
532 }
533
534 #[rstest]
535 fn checksum_sha1() {
536 let data = "foo\n";
537 let digest = vec![
538 241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
539 236, 21,
540 ];
541 let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
542
543 let checksum = Sha1Checksum::calculate_from(data);
544 assert_eq!(digest, checksum.inner());
545 assert_eq!(format!("{}", &checksum), hex_digest,);
546
547 let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
548 assert_eq!(digest, checksum.inner());
549 assert_eq!(format!("{}", &checksum), hex_digest,);
550 }
551
552 #[rstest]
553 fn checksum_sha224() {
554 let data = "foo\n";
555 let digest = vec![
556 231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
557 127, 102, 41, 189, 195, 40, 111, 157, 210,
558 ];
559 let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
560
561 let checksum = Sha224Checksum::calculate_from(data);
562 assert_eq!(digest, checksum.inner());
563 assert_eq!(format!("{}", &checksum), hex_digest,);
564
565 let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
566 assert_eq!(digest, checksum.inner());
567 assert_eq!(format!("{}", &checksum), hex_digest,);
568 }
569
570 #[rstest]
571 fn checksum_sha256() {
572 let data = "foo\n";
573 let digest = vec![
574 181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
575 82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
576 ];
577 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
578
579 let checksum = Sha256Checksum::calculate_from(data);
580 assert_eq!(digest, checksum.inner());
581 assert_eq!(format!("{}", &checksum), hex_digest,);
582
583 let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
584 assert_eq!(digest, checksum.inner());
585 assert_eq!(format!("{}", &checksum), hex_digest,);
586 }
587
588 #[rstest]
589 fn checksum_sha384() {
590 let data = "foo\n";
591 let digest = vec![
592 142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
593 6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
594 232, 247, 152, 40, 155, 26, 13, 30, 6,
595 ];
596 let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
597
598 let checksum = Sha384Checksum::calculate_from(data);
599 assert_eq!(digest, checksum.inner());
600 assert_eq!(format!("{}", &checksum), hex_digest,);
601
602 let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
603 assert_eq!(digest, checksum.inner());
604 assert_eq!(format!("{}", &checksum), hex_digest,);
605 }
606
607 #[rstest]
608 fn checksum_sha512() {
609 let data = "foo\n";
610 let digest = vec![
611 12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
612 114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
613 25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
614 138, 81, 182, 232, 180, 230, 182,
615 ];
616 let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
617
618 let checksum = Sha512Checksum::calculate_from(data);
619 assert_eq!(digest, checksum.inner());
620 assert_eq!(format!("{}", &checksum), hex_digest);
621
622 let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
623 assert_eq!(digest, checksum.inner());
624 assert_eq!(format!("{}", &checksum), hex_digest);
625 }
626
627 #[rstest]
628 #[case::non_hex_digits(
629 "0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6",
630 "expected ASCII hex digit"
631 )]
632 #[case::incomplete_pair(" b ", "expected ASCII hex digit")]
633 #[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca", "expected ASCII hex digit")]
634 #[case::whitespace(
635 "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",
636 "expected ASCII hex digit"
637 )]
638 fn checksum_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
639 let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
640 panic!("'{input}' did not fail to parse as expected")
641 };
642 assert!(
643 err_msg.contains(err_snippet),
644 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
645 );
646 }
647
648 #[rstest]
649 fn skippable_checksum_sha256() {
650 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
651 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
652 assert_eq!(format!("{}", &checksum), hex_digest);
653 }
654
655 #[rstest]
656 fn skippable_checksum_skip() {
657 let hex_digest = "SKIP";
658 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
659
660 assert_eq!(SkippableChecksum::Skip, checksum);
661 assert_eq!(format!("{}", &checksum), hex_digest);
662 }
663}