Documentation
use crc32fast::Hasher as Crc32Hasher;
use std::{
    io,
    io::Read,
    path::{Path, PathBuf},
};

pub fn hash(mut reader: impl Read, buffer: &mut [u8]) -> Result<u32, io::Error> {
    let mut hasher = Crc32Hasher::new();

    while let Ok(bytes_read) = reader.read(buffer) {
        if bytes_read == 0 {
            break;
        }
        hasher.update(&buffer[..bytes_read]);
    }

    Ok(hasher.finalize())
}

pub fn hash_file_contents(path: impl AsRef<Path>, buffer: &mut [u8]) -> Result<u32, io::Error> {
    let file = std::fs::File::open(path.as_ref())?;
    hash(file, buffer)
}

pub fn walk_dir(dir: &Path, on_file_found: &mut dyn FnMut(&PathBuf)) -> io::Result<()> {
    if !dir.is_dir() {
        return Ok(());
    }

    for entry in std::fs::read_dir(dir)? {
        let path = entry?.path();
        if path.is_dir() {
            walk_dir(&path, on_file_found)?;
        } else {
            on_file_found(&path);
        }
    }

    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;
    use std::io::Cursor;

    const TEST_TEXT: &[u8] = b"hello world";
    const TEST_TEXT_HASH: u32 = 0xD4A1185;

    #[test]
    fn test_hash() {
        let mut buffer = [0; 1024];
        {
            let mut cursor = Cursor::new(TEST_TEXT.to_vec());
            let hash = hash(&mut cursor, &mut buffer).unwrap();
            assert_eq!(hash, TEST_TEXT_HASH);
        }
        {
            let mut cursor = Cursor::new(b"world, hello ".to_vec());
            let hash = hash(&mut cursor, &mut buffer).unwrap();
            assert_ne!(hash, TEST_TEXT_HASH);
        }
    }

    #[test]
    fn test_hash_file_contents() {
        let mut buffer = [0; 1024];

        let dir = tempfile::tempdir().unwrap();
        let file = dir.path().join("hashed.txt");
        std::fs::write(&file, TEST_TEXT).unwrap();

        let hash = hash_file_contents(file.to_str().unwrap(), &mut buffer).unwrap();
        assert_eq!(hash, TEST_TEXT_HASH);

        std::fs::remove_file(file).unwrap();
    }
}