warg-crypto 0.10.0

A collection of cryptographic primitives for Warg registries.
Documentation
use super::{Digest, HashAlgorithm, Sha256};
use anyhow::Error;
use serde::{Deserialize, Serialize};
use std::{fmt, ops::Deref, str::FromStr};
use thiserror::Error;

pub enum Hasher {
    Sha256(Sha256),
}

impl Hasher {
    pub fn update(&mut self, bytes: &[u8]) {
        match self {
            Self::Sha256(d) => d.update(bytes),
        }
    }

    pub fn finalize(self) -> AnyHash {
        let (algo, bytes) = match self {
            Self::Sha256(d) => (HashAlgorithm::Sha256, d.finalize().deref().into()),
        };

        AnyHash { algo, bytes }
    }
}

impl HashAlgorithm {
    pub fn hasher(&self) -> Hasher {
        match self {
            HashAlgorithm::Sha256 => Hasher::Sha256(Sha256::new()),
        }
    }

    pub fn digest(&self, content_bytes: &[u8]) -> AnyHash {
        let hash_bytes: Vec<u8> = match self {
            HashAlgorithm::Sha256 => {
                let mut d = Sha256::new();
                d.update(content_bytes);
                d.finalize().deref().into()
            }
        };

        AnyHash {
            algo: *self,
            bytes: hash_bytes,
        }
    }
}

#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct AnyHash {
    pub(crate) algo: HashAlgorithm,
    pub(crate) bytes: Vec<u8>,
}

impl AnyHash {
    pub fn new(algo: HashAlgorithm, bytes: Vec<u8>) -> AnyHash {
        AnyHash { algo, bytes }
    }

    pub fn algorithm(&self) -> HashAlgorithm {
        self.algo
    }

    pub fn bytes(&self) -> &[u8] {
        &self.bytes
    }
}

impl fmt::Display for AnyHash {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}:{}", self.algo, hex::encode(self.bytes.as_slice()))
    }
}

impl fmt::Debug for AnyHash {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}:{}", self.algo, hex::encode(self.bytes.as_slice()))
    }
}

impl FromStr for AnyHash {
    type Err = AnyHashError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (algo_part, bytes_part) = s
            .split_once(':')
            .ok_or_else(|| AnyHashError::IncorrectStructure(s.matches(':').count() + 1))?;

        if bytes_part.chars().any(|c| "ABCDEF".contains(c)) {
            return Err(AnyHashError::UppercaseHex);
        }

        let algo = algo_part.parse::<HashAlgorithm>()?;
        let bytes = hex::decode(bytes_part)?;

        Ok(AnyHash { algo, bytes })
    }
}

#[derive(Error, Debug)]
pub enum AnyHashError {
    #[error("expected two parts for hash; found {0}")]
    IncorrectStructure(usize),

    #[error("unable to parse hash algorithm: {0}")]
    InvalidHashAlgorithm(#[from] Error),

    #[error("hash contained uppercase hex values")]
    UppercaseHex,

    #[error("hexadecimal decode failed: {0}")]
    InvalidHex(#[from] hex::FromHexError),
}

impl Serialize for AnyHash {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for AnyHash {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        Self::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sha256_labeled_digest() {
        let input = b"The quick brown fox jumped over the lazy dog";
        let output = HashAlgorithm::Sha256.digest(input);
        let output = format!("{}", output);

        let expected = "sha256:7d38b5cd25a2baf85ad3bb5b9311383e671a8a142eb302b324d4a5fba8748c69";

        assert_eq!(output, expected)
    }

    #[test]
    fn test_labeled_digest_parse_rejects_uppercase() {
        let digest_str = "sha256:7d38b5cd25a2baf85ad3bb5b9311383e671a8a142eb302b324d4a5fba8748c69";
        assert!(digest_str.parse::<AnyHash>().is_ok());

        let (algo, encoded) = digest_str.split_once(':').unwrap();
        let digest_str = String::from(algo) + ":" + &encoded.to_uppercase();
        assert!(digest_str.parse::<AnyHash>().is_err());
    }

    #[test]
    fn test_labeled_digest_roundtrip() {
        let input = "sha256:7d38b5cd25a2baf85ad3bb5b9311383e671a8a142eb302b324d4a5fba8748c69";
        let output = format!("{}", input.parse::<AnyHash>().unwrap());
        assert_eq!(input, &output);
    }
}