hashjunkie 0.5.2

Fast multi-algorithm hashing library with file-sharing and cloud hash support
Documentation
use digest::Digest;

use crate::{DigestValue, base32_upper_no_padding, hashes::Hasher};

const LEAF_SIZE: usize = 1024;
const TIGER_DIGEST_SIZE: usize = 24;

pub struct TigerTreeHasher {
    leaves: Vec<[u8; TIGER_DIGEST_SIZE]>,
    current_leaf: Vec<u8>,
}

impl TigerTreeHasher {
    pub fn new() -> Self {
        Self {
            leaves: Vec::new(),
            current_leaf: Vec::with_capacity(LEAF_SIZE),
        }
    }

    fn push_current_leaf(&mut self) {
        let leaf = std::mem::replace(&mut self.current_leaf, Vec::with_capacity(LEAF_SIZE));
        self.leaves.push(tiger_leaf(&leaf));
    }
}

impl Default for TigerTreeHasher {
    fn default() -> Self {
        Self::new()
    }
}

impl Hasher for TigerTreeHasher {
    fn update(&mut self, mut data: &[u8]) {
        if self.current_leaf.len() == LEAF_SIZE && !data.is_empty() {
            self.push_current_leaf();
        }

        while !data.is_empty() {
            let remaining = LEAF_SIZE - self.current_leaf.len();
            let take = data.len().min(remaining);
            self.current_leaf.extend_from_slice(&data[..take]);
            data = &data[take..];

            if self.current_leaf.len() == LEAF_SIZE && !data.is_empty() {
                self.push_current_leaf();
            }
        }
    }

    fn finalize_hex(self: Box<Self>) -> String {
        self.finalize_digest().standard().to_string()
    }

    fn finalize_digest(mut self: Box<Self>) -> DigestValue {
        if self.leaves.is_empty() || !self.current_leaf.is_empty() {
            self.push_current_leaf();
        }

        let mut level = self.leaves;
        while level.len() > 1 {
            level = level
                .chunks(2)
                .map(|chunk| {
                    if chunk.len() == 1 {
                        chunk[0]
                    } else {
                        tiger_node(&chunk[0], &chunk[1])
                    }
                })
                .collect();
        }

        DigestValue::from_raw_standard(level[0], base32_upper_no_padding(&level[0]))
    }
}

fn tiger_leaf(data: &[u8]) -> [u8; TIGER_DIGEST_SIZE] {
    let mut tiger = tiger::Tiger::new();
    tiger.update([0x00]);
    tiger.update(data);
    tiger.finalize().into()
}

fn tiger_node(left: &[u8; TIGER_DIGEST_SIZE], right: &[u8; TIGER_DIGEST_SIZE]) -> [u8; 24] {
    let mut tiger = tiger::Tiger::new();
    tiger.update([0x01]);
    tiger.update(left);
    tiger.update(right);
    tiger.finalize().into()
}

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

    fn hash(data: &[u8]) -> String {
        let mut h = TigerTreeHasher::new();
        h.update(data);
        Box::new(h).finalize_hex()
    }

    #[test]
    fn empty_matches_known_gnutella_tiger_tree_hash() {
        assert_eq!(hash(b""), "LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ");
    }

    #[test]
    fn default_equals_new() {
        let mut default_hasher = TigerTreeHasher::default();
        default_hasher.update(b"abc");

        let mut new_hasher = TigerTreeHasher::new();
        new_hasher.update(b"abc");

        assert_eq!(
            Box::new(default_hasher).finalize_hex(),
            Box::new(new_hasher).finalize_hex()
        );
    }

    #[test]
    fn single_leaf_hash_is_base32_tiger_of_prefixed_data() {
        assert_eq!(hash(b"abc"), base32_upper_no_padding(&tiger_leaf(b"abc")));
    }

    #[test]
    fn chunked_update_matches_single_update() {
        let data = (0..(LEAF_SIZE * 3 + 17))
            .map(|i| (i % 251) as u8)
            .collect::<Vec<_>>();
        let single = hash(&data);

        let mut h = TigerTreeHasher::new();
        for chunk in data.chunks(333) {
            h.update(chunk);
        }

        assert_eq!(Box::new(h).finalize_hex(), single);
    }

    #[test]
    fn one_byte_over_leaf_boundary_uses_internal_node() {
        let mut data = vec![0xA5; LEAF_SIZE];
        data.push(0x5A);

        let left = tiger_leaf(&data[..LEAF_SIZE]);
        let right = tiger_leaf(&data[LEAF_SIZE..]);

        assert_eq!(
            hash(&data),
            base32_upper_no_padding(&tiger_node(&left, &right))
        );
    }

    #[test]
    fn new_update_after_exact_leaf_flushes_deferred_leaf() {
        let mut h = TigerTreeHasher::new();
        h.update(&vec![0xA5; LEAF_SIZE]);
        h.update(&[0x5A]);

        let mut data = vec![0xA5; LEAF_SIZE];
        data.push(0x5A);

        assert_eq!(Box::new(h).finalize_hex(), hash(&data));
    }
}