use std::{
fmt::{Debug, Display, Formatter},
marker::PhantomData,
ops::DerefMut,
str::FromStr,
};
use digest::{Digest, FixedOutput, HashMarker, Output, OutputSizeUser, Update};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use strum::{Display, EnumString, VariantArray, VariantNames};
use winnow::{
ModalResult,
Parser,
ascii::dec_uint,
combinator::{alt, cut_err, eof, repeat, terminated},
error::{StrContext, StrContextValue},
token::one_of,
};
use crate::{
Error,
digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DigestEncoding {
Hex,
Dec,
}
pub trait DigestString: Digest {
const ENCODING: DigestEncoding;
}
impl DigestString for Blake2b512 {
const ENCODING: DigestEncoding = DigestEncoding::Hex;
}
impl DigestString for Md5 {
const ENCODING: DigestEncoding = DigestEncoding::Hex;
}
impl DigestString for Sha1 {
const ENCODING: DigestEncoding = DigestEncoding::Hex;
}
impl DigestString for Sha224 {
const ENCODING: DigestEncoding = DigestEncoding::Hex;
}
impl DigestString for Sha256 {
const ENCODING: DigestEncoding = DigestEncoding::Hex;
}
impl DigestString for Sha384 {
const ENCODING: DigestEncoding = DigestEncoding::Hex;
}
impl DigestString for Sha512 {
const ENCODING: DigestEncoding = DigestEncoding::Hex;
}
impl DigestString for Crc32Cksum {
const ENCODING: DigestEncoding = DigestEncoding::Dec;
}
pub type Blake2b512Checksum = Checksum<Blake2b512>;
pub type Md5Checksum = Checksum<Md5>;
pub type Sha1Checksum = Checksum<Sha1>;
pub type Sha224Checksum = Checksum<Sha224>;
pub type Sha256Checksum = Checksum<Sha256>;
pub type Sha384Checksum = Checksum<Sha384>;
pub type Sha512Checksum = Checksum<Sha512>;
pub type Crc32CksumChecksum = Checksum<Crc32Cksum>;
#[derive(
Clone,
Copy,
Debug,
Deserialize,
Display,
EnumString,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
VariantNames,
VariantArray,
)]
pub enum ChecksumAlgorithm {
Blake2b512,
Md5,
Sha1,
Sha224,
Sha256,
Sha384,
Sha512,
Crc32Cksum,
}
impl ChecksumAlgorithm {
pub fn is_deprecated(&self) -> bool {
match self {
ChecksumAlgorithm::Md5 | ChecksumAlgorithm::Sha1 | ChecksumAlgorithm::Crc32Cksum => {
true
}
ChecksumAlgorithm::Blake2b512
| ChecksumAlgorithm::Sha224
| ChecksumAlgorithm::Sha256
| ChecksumAlgorithm::Sha384
| ChecksumAlgorithm::Sha512 => false,
}
}
pub fn non_deprecated_checksums(&self) -> Vec<ChecksumAlgorithm> {
<ChecksumAlgorithm as VariantArray>::VARIANTS
.iter()
.filter(|algo| !algo.is_deprecated())
.copied()
.collect::<Vec<ChecksumAlgorithm>>()
}
}
#[derive(Clone)]
pub struct Checksum<D: Digest> {
digest: Vec<u8>,
_marker: PhantomData<D>,
}
impl<D: DigestString> Serialize for Checksum<D> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de, D: DigestString> Deserialize<'de> for Checksum<D> {
fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
where
De: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Checksum::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl<D: DigestString> Checksum<D> {
pub fn calculate_from(input: impl AsRef<[u8]>) -> Self {
let mut hasher = D::new();
hasher.update(input);
Checksum {
digest: hasher.finalize()[..].to_vec(),
_marker: PhantomData,
}
}
pub fn inner(&self) -> &[u8] {
&self.digest
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
#[inline]
fn hex_digit(input: &mut &str) -> ModalResult<u8> {
one_of(('0'..='9', 'a'..='f', 'A'..='F'))
.map(|d: char|
d.to_digit(16).unwrap().try_into().unwrap())
.context(StrContext::Expected(StrContextValue::Description(
"ASCII hex digit",
)))
.parse_next(input)
}
let hex_pair = (hex_digit, hex_digit).map(|(first, second)|
(first << 4) + second);
let digest_bytes = <D as Digest>::output_size();
let digest = match D::ENCODING {
DigestEncoding::Hex => {
let digest = cut_err(repeat(digest_bytes, hex_pair))
.context(StrContext::Label("hash digest"))
.context(StrContext::Expected(StrContextValue::Description(
"a hex hash digest with the appropriate length for the given algorithm.",
)))
.parse_next(input)?;
cut_err(eof)
.context(StrContext::Expected(StrContextValue::Description(
"end of checksum. Checksum is too long.",
)))
.parse_next(input)?;
digest
}
DigestEncoding::Dec => {
let digest_bits = digest_bytes * 8;
let max_value: u128 = if digest_bits >= 128 {
u128::MAX
} else {
(1u128 << digest_bits) - 1
};
cut_err(dec_uint::<_, u128, _>)
.verify(move |&v| v <= max_value)
.map(move |v | v.to_be_bytes()[16 - digest_bytes..].to_vec())
.context(StrContext::Label("hash digest"))
.context(StrContext::Expected(StrContextValue::Description(
"a decimal hash digest with the appropriate length for the given algorithm.",
)))
.parse_next(input)?
}
};
Ok(Self {
digest,
_marker: PhantomData,
})
}
}
impl<D: DigestString> FromStr for Checksum<D> {
type Err = Error;
fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
Ok(Checksum::parser.parse(s)?)
}
}
impl<D: DigestString> Display for Checksum<D> {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
match D::ENCODING {
DigestEncoding::Hex => {
write!(
fmt,
"{}",
self.digest
.iter()
.map(|x| format!("{x:02x?}"))
.collect::<Vec<String>>()
.join("")
)
}
DigestEncoding::Dec => {
let value = self
.digest
.iter()
.fold(0u128, |acc, &byte| (acc << 8) | byte as u128);
write!(fmt, "{}", value)
}
}
}
}
impl<D: DigestString> Debug for Checksum<D> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self, f)
}
}
impl<D: Digest> PartialEq for Checksum<D> {
fn eq(&self, other: &Self) -> bool {
self.digest == other.digest
}
}
impl<D: Digest> Eq for Checksum<D> {}
impl<D: Digest> Ord for Checksum<D> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.digest.cmp(&other.digest)
}
}
impl<D: Digest> PartialOrd for Checksum<D> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum SkippableChecksum<D: DigestString + Clone> {
Skip,
#[serde(bound = "D: Digest + Clone")]
Checksum {
digest: Checksum<D>,
},
}
impl<D: DigestString + Clone> SkippableChecksum<D> {
pub fn is_skipped(&self) -> bool {
matches!(self, SkippableChecksum::Skip)
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
terminated(
alt((
"SKIP".value(Self::Skip),
Checksum::parser.map(|digest| Self::Checksum { digest }),
)),
cut_err(eof).context(StrContext::Expected(StrContextValue::Description(
"end of checksum.",
))),
)
.parse_next(input)
}
}
impl<D: DigestString + Clone> FromStr for SkippableChecksum<D> {
type Err = Error;
fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl<D: DigestString + Clone> Display for SkippableChecksum<D> {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
let output = match self {
SkippableChecksum::Skip => "SKIP".to_string(),
SkippableChecksum::Checksum { digest } => digest.to_string(),
};
write!(fmt, "{output}",)
}
}
impl<D: DigestString + Clone> PartialEq for SkippableChecksum<D> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
(SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
(SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
(
SkippableChecksum::Checksum { digest },
SkippableChecksum::Checksum {
digest: digest_other,
},
) => digest == digest_other,
}
}
}
#[derive(Clone, Debug)]
pub struct Crc32Cksum {
digest: crc_fast::Digest,
len: u64,
}
impl HashMarker for Crc32Cksum {}
impl Default for Crc32Cksum {
fn default() -> Self {
Self {
digest: crc_fast::Digest::new(crc_fast::CrcAlgorithm::Crc32Cksum),
len: 0,
}
}
}
impl Update for Crc32Cksum {
fn update(&mut self, data: &[u8]) {
self.digest.update(data);
self.len += data.len() as u64;
}
}
impl OutputSizeUser for Crc32Cksum {
type OutputSize = digest::consts::U4;
}
impl FixedOutput for Crc32Cksum {
fn finalize_into(mut self, out: &mut Output<Self>) {
if self.len != 0 {
let len_bytes = self.len.to_be_bytes();
let start = len_bytes.iter().position(|&b| b != 0).unwrap_or(7);
self.digest.update(&len_bytes[start..]);
}
let crc = self.digest.finalize() as u32;
out.deref_mut().clone_from_slice(&crc.to_be_bytes());
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rstest::rstest;
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
}
#[test]
fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
assert!(Blake2b512Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
assert!(Blake2b512Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
assert!(Blake2b512Checksum::from_str(&string).is_err());
}
#[test]
fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
}
#[test]
fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
assert!(Sha1Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
assert!(Sha1Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
assert!(Sha1Checksum::from_str(&string).is_err());
}
#[test]
fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
}
#[test]
fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
assert!(Sha224Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
assert!(Sha224Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
assert!(Sha224Checksum::from_str(&string).is_err());
}
#[test]
fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
}
#[test]
fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
assert!(Sha256Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
assert!(Sha256Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
assert!(Sha256Checksum::from_str(&string).is_err());
}
#[test]
fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
}
#[test]
fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
assert!(Sha384Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
assert!(Sha384Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
assert!(Sha384Checksum::from_str(&string).is_err());
}
#[test]
fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
}
#[test]
fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
assert!(Sha512Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
assert!(Sha512Checksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
assert!(Sha512Checksum::from_str(&string).is_err());
}
#[test]
fn valid_checksum_crc32cksum(sum in 0u32..=u32::MAX) {
let decimal_str = format!("{sum}");
prop_assert_eq!(
&decimal_str,
&format!("{}", Crc32CksumChecksum::from_str(decimal_str.as_str()).unwrap())
);
}
#[test]
fn invalid_checksum_crc32cksum_bigger_size(sum in (u32::MAX as u128)..=u128::MAX) {
let decimal_str = format!("{sum}");
assert!(Crc32CksumChecksum::from_str(decimal_str.as_str()).is_err());
}
#[test]
fn invalid_checksum_crc32cksum_wrong_chars(string in r"[a-f]{9}") {
assert!(Crc32CksumChecksum::from_str(&string).is_err());
}
#[test]
fn invalid_checksum_crc32cksum_negative(string in r"-[1-9]{9}") {
assert!(Crc32CksumChecksum::from_str(&string).is_err());
}
}
#[rstest]
fn checksum_blake2b512() {
let data = "foo\n";
let digest = vec![
210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
217, 171, 101, 39, 142, 119,
];
let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
let checksum = Blake2b512Checksum::calculate_from(data);
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
}
#[rstest]
fn checksum_sha1() {
let data = "foo\n";
let digest = vec![
241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
236, 21,
];
let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
let checksum = Sha1Checksum::calculate_from(data);
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
}
#[rstest]
fn checksum_sha224() {
let data = "foo\n";
let digest = vec![
231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
127, 102, 41, 189, 195, 40, 111, 157, 210,
];
let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
let checksum = Sha224Checksum::calculate_from(data);
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
}
#[rstest]
fn checksum_sha256() {
let data = "foo\n";
let digest = vec![
181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
];
let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
let checksum = Sha256Checksum::calculate_from(data);
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
}
#[rstest]
fn checksum_sha384() {
let data = "foo\n";
let digest = vec![
142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
232, 247, 152, 40, 155, 26, 13, 30, 6,
];
let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
let checksum = Sha384Checksum::calculate_from(data);
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest,);
}
#[rstest]
fn checksum_sha512() {
let data = "foo\n";
let digest = vec![
12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
138, 81, 182, 232, 180, 230, 182,
];
let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
let checksum = Sha512Checksum::calculate_from(data);
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest);
let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
assert_eq!(digest, checksum.inner());
assert_eq!(format!("{}", &checksum), hex_digest);
}
#[rstest]
fn checksum_crc32cksum() {
let data = "foo\n";
let digest = 3915528286u32;
let digest_string = format!("{digest}");
let checksum = Crc32CksumChecksum::calculate_from(data);
assert_eq!(digest.to_be_bytes(), checksum.inner());
assert_eq!(format!("{}", &checksum), digest_string);
let checksum = Crc32CksumChecksum::from_str(digest_string.as_str()).unwrap();
assert_eq!(digest.to_be_bytes(), checksum.inner());
assert_eq!(format!("{}", &checksum), digest_string);
}
#[rstest]
#[case::non_hex_digits(
"0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6",
"expected ASCII hex digit"
)]
#[case::incomplete_pair(" b ", "expected ASCII hex digit")]
#[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca", "expected ASCII hex digit")]
#[case::whitespace(
"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",
"expected ASCII hex digit"
)]
fn checksum_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
panic!("'{input}' did not fail to parse as expected")
};
assert!(
err_msg.contains(err_snippet),
"Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
);
}
#[rstest]
fn skippable_checksum_sha256() {
let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
assert_eq!(format!("{}", &checksum), hex_digest);
}
#[rstest]
fn skippable_checksum_skip() {
let hex_digest = "SKIP";
let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
assert_eq!(SkippableChecksum::Skip, checksum);
assert_eq!(format!("{}", &checksum), hex_digest);
}
}