aria2-core 0.2.2

High-performance download engine core: multi-protocol segmented downloads, rate limiting, config management, session persistence, and BitTorrent seeding
Documentation
use digest::Digest;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HashType {
    Md5,
    Sha1,
    Sha256,
    Sha512,
    Adler32,
}

impl HashType {
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(s: &str) -> Option<HashType> {
        match s.to_lowercase().as_str() {
            "md5" => Some(HashType::Md5),
            "sha-1" | "sha1" => Some(HashType::Sha1),
            "sha-256" | "sha256" => Some(HashType::Sha256),
            "sha-512" | "sha512" => Some(HashType::Sha512),
            "adler32" => Some(HashType::Adler32),
            _ => None,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            HashType::Md5 => "md5",
            HashType::Sha1 => "sha1",
            HashType::Sha256 => "sha256",
            HashType::Sha512 => "sha512",
            HashType::Adler32 => "adler32",
        }
    }

    pub fn digest_length(&self) -> usize {
        match self {
            HashType::Md5 => 16,
            HashType::Sha1 => 20,
            HashType::Sha256 => 32,
            HashType::Sha512 => 64,
            HashType::Adler32 => 4,
        }
    }

    pub fn all_supported() -> Vec<HashType> {
        vec![
            HashType::Md5,
            HashType::Sha1,
            HashType::Sha256,
            HashType::Sha512,
            HashType::Adler32,
        ]
    }
}

enum DigestInner {
    Md5(md5::Context),
    Sha1(sha1::Sha1),
    Sha256(sha2::Sha256),
    Sha512(sha2::Sha512),
    Adler32(Vec<u8>),
}

pub struct MessageDigest {
    inner: DigestInner,
}

impl MessageDigest {
    pub fn new(algo: HashType) -> Self {
        let inner = match algo {
            HashType::Md5 => DigestInner::Md5(md5::Context::new()),
            HashType::Sha1 => DigestInner::Sha1(sha1::Sha1::new()),
            HashType::Sha256 => DigestInner::Sha256(sha2::Sha256::new()),
            HashType::Sha512 => DigestInner::Sha512(sha2::Sha512::new()),
            HashType::Adler32 => DigestInner::Adler32(Vec::new()),
        };
        MessageDigest { inner }
    }

    pub fn update(&mut self, data: &[u8]) {
        match &mut self.inner {
            DigestInner::Md5(d) => d.consume(data),
            DigestInner::Sha1(d) => d.update(data),
            DigestInner::Sha256(d) => d.update(data),
            DigestInner::Sha512(d) => d.update(data),
            DigestInner::Adler32(buf) => buf.extend_from_slice(data),
        }
    }

    pub fn finalize(self) -> Vec<u8> {
        match self.inner {
            DigestInner::Md5(d) => d.compute().to_vec(),
            DigestInner::Sha1(d) => d.finalize().to_vec(),
            DigestInner::Sha256(d) => d.finalize().to_vec(),
            DigestInner::Sha512(d) => d.finalize().to_vec(),
            DigestInner::Adler32(buf) => {
                let checksum = adler32::adler32(&buf[..]).unwrap_or(1);
                checksum.to_le_bytes().to_vec()
            }
        }
    }

    pub fn finalize_hex(self) -> String {
        let bytes = self.finalize();
        bytes.iter().map(|b| format!("{:02x}", b)).collect()
    }

    pub fn digest_length(&self) -> usize {
        match &self.inner {
            DigestInner::Md5(_) => 16,
            DigestInner::Sha1(_) => 20,
            DigestInner::Sha256(_) => 32,
            DigestInner::Sha512(_) => 64,
            DigestInner::Adler32(_) => 4,
        }
    }

    pub fn reset(&mut self) {
        match &mut self.inner {
            DigestInner::Md5(d) => *d = md5::Context::new(),
            DigestInner::Sha1(d) => *d = sha1::Sha1::new(),
            DigestInner::Sha256(d) => *d = sha2::Sha256::new(),
            DigestInner::Sha512(d) => *d = sha2::Sha512::new(),
            DigestInner::Adler32(s) => *s = Vec::new(),
        }
    }

    pub fn hash_data(algo: HashType, data: &[u8]) -> Vec<u8> {
        let mut digest = Self::new(algo);
        digest.update(data);
        digest.finalize()
    }

    pub fn hash_hex(algo: HashType, data: &[u8]) -> String {
        let mut digest = Self::new(algo);
        digest.update(data);
        digest.finalize_hex()
    }
}

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

    #[test]
    fn test_hash_type_from_str() {
        assert_eq!(HashType::from_str("md5"), Some(HashType::Md5));
        assert_eq!(HashType::from_str("MD5"), Some(HashType::Md5));
        assert_eq!(HashType::from_str("sha-1"), Some(HashType::Sha1));
        assert_eq!(HashType::from_str("SHA1"), Some(HashType::Sha1));
        assert_eq!(HashType::from_str("sha-256"), Some(HashType::Sha256));
        assert_eq!(HashType::from_str("sha256"), Some(HashType::Sha256));
        assert_eq!(HashType::from_str("sha-512"), Some(HashType::Sha512));
        assert_eq!(HashType::from_str("adler32"), Some(HashType::Adler32));
        assert_eq!(HashType::from_str("unknown"), None);
    }

    #[test]
    fn test_md5_known_vector() {
        let hex = MessageDigest::hash_hex(HashType::Md5, b"");
        assert_eq!(hex, "d41d8cd98f00b204e9800998ecf8427e");

        let hex = MessageDigest::hash_hex(HashType::Md5, b"hello world");
        assert_eq!(hex, "5eb63bbbe01eeed093cb22bb8f5acdc3");
    }

    #[test]
    fn test_sha1_known_vector() {
        let hex = MessageDigest::hash_hex(HashType::Sha1, b"");
        assert_eq!(hex, "da39a3ee5e6b4b0d3255bfef95601890afd80709");

        let hex = MessageDigest::hash_hex(
            HashType::Sha1,
            b"The quick brown fox jumps over the lazy dog",
        );
        assert_eq!(hex, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12");
    }

    #[test]
    fn test_sha256_known_vector() {
        let hex = MessageDigest::hash_hex(HashType::Sha256, b"");
        assert_eq!(
            hex,
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );

        let hex = MessageDigest::hash_hex(HashType::Sha256, b"abc");
        assert_eq!(
            hex,
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }

    #[test]
    fn test_adler32_basic() {
        let bytes = MessageDigest::hash_data(HashType::Adler32, b"hello world");
        assert_eq!(bytes.len(), 4);

        let empty = MessageDigest::hash_data(HashType::Adler32, b"");
        assert_eq!(empty.len(), 4);
    }

    #[test]
    fn test_finalize_hex_format_lowercase() {
        let hex = MessageDigest::hash_hex(HashType::Md5, b"test");
        for ch in hex.chars() {
            assert!(
                ch.is_ascii_digit() || ('a'..='f').contains(&ch),
                "hex should be lowercase: {}",
                hex
            );
        }
    }

    #[test]
    fn test_digest_length_matches() {
        for ht in HashType::all_supported() {
            let md = MessageDigest::new(ht);
            assert_eq!(md.digest_length(), ht.digest_length());
        }
    }

    #[test]
    fn test_streaming_vs_one_shot() {
        let one_shot = MessageDigest::hash_hex(HashType::Sha256, b"hello world");

        let mut streaming = MessageDigest::new(HashType::Sha256);
        streaming.update(b"hello ");
        streaming.update(b"world");
        let streaming_hex = streaming.finalize_hex();

        assert_eq!(one_shot, streaming_hex);
    }

    #[test]
    fn test_different_data_different_hash() {
        let h1 = MessageDigest::hash_hex(HashType::Md5, b"first data");
        let h2 = MessageDigest::hash_hex(HashType::Md5, b"second data");
        assert_ne!(h1, h2, "different data should produce different hash value");
    }
}