use sha2::{Digest, Sha256};
use std::fs;
use std::io;
use std::path::Path;
use crate::error::Result;
pub const HASH_LEN: usize = 32;
pub const HASH_HEX_LEN: usize = 64;
pub fn hash_bytes(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:x}", hasher.finalize())
}
pub fn hash_bytes_raw(data: &[u8]) -> [u8; HASH_LEN] {
let mut hasher = Sha256::new();
hasher.update(data);
let mut out = [0u8; HASH_LEN];
out.copy_from_slice(&hasher.finalize());
out
}
pub fn hash_file(path: &Path) -> Result<String> {
let mut file = fs::File::open(path)?;
let mut hasher = Sha256::new();
io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.finalize()))
}
pub fn hash_reader(reader: &mut impl io::Read) -> Result<String> {
let mut hasher = Sha256::new();
io::copy(reader, &mut hasher)?;
Ok(format!("{:x}", hasher.finalize()))
}
pub fn verify_hash(data: &[u8], expected_hex: &str) -> bool {
hash_bytes(data) == expected_hex
}
pub fn verify_file_hash(path: &Path, expected_hex: &str) -> Result<bool> {
let actual = hash_file(path)?;
Ok(actual == expected_hex)
}
pub struct StreamingHasher {
hasher: Sha256,
}
impl StreamingHasher {
pub fn new() -> Self {
Self {
hasher: Sha256::new(),
}
}
pub fn update(&mut self, data: &[u8]) {
self.hasher.update(data);
}
pub fn finalize(self) -> String {
format!("{:x}", self.hasher.finalize())
}
pub fn finalize_raw(self) -> [u8; HASH_LEN] {
let mut out = [0u8; HASH_LEN];
out.copy_from_slice(&self.hasher.finalize());
out
}
}
impl Default for StreamingHasher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_hash_bytes_known_answer() {
assert_eq!(
hash_bytes(b"hello world"),
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
#[test]
fn test_hash_bytes_empty() {
assert_eq!(
hash_bytes(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn test_hash_bytes_raw_length() {
let raw = hash_bytes_raw(b"test");
assert_eq!(raw.len(), HASH_LEN);
}
#[test]
fn test_hash_file() -> Result<()> {
let tmp = TempDir::new()?;
let path = tmp.path().join("data.bin");
fs::write(&path, b"hello world")?;
let hash = hash_file(&path)?;
assert_eq!(
hash,
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
Ok(())
}
#[test]
fn test_hash_file_large() -> Result<()> {
let tmp = TempDir::new()?;
let path = tmp.path().join("large.bin");
let data = vec![0x42u8; 10 * 1024 * 1024];
fs::write(&path, &data)?;
let hash = hash_file(&path)?;
assert_eq!(hash.len(), HASH_HEX_LEN);
assert_eq!(hash, hash_bytes(&data));
Ok(())
}
#[test]
fn test_verify_hash_match() {
let data = b"verify me";
let hash = hash_bytes(data);
assert!(verify_hash(data, &hash));
}
#[test]
fn test_verify_hash_mismatch() {
assert!(!verify_hash(b"real data", "deadbeef"));
}
#[test]
fn test_verify_file_hash() -> Result<()> {
let tmp = TempDir::new()?;
let path = tmp.path().join("verify.bin");
fs::write(&path, b"trust but verify")?;
let expected = hash_bytes(b"trust but verify");
assert!(verify_file_hash(&path, &expected)?);
Ok(())
}
#[test]
fn test_streaming_hasher() {
let mut hasher = StreamingHasher::new();
hasher.update(b"hello ");
hasher.update(b"world");
let hash = hasher.finalize();
assert_eq!(
hash,
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
#[test]
fn test_streaming_hasher_matches_oneshot() {
let chunks: Vec<&[u8]> = vec![b"chunk1", b"chunk2", b"chunk3", b"chunk4"];
let combined: Vec<u8> = chunks.iter().flat_map(|c| c.iter()).copied().collect();
let mut hasher = StreamingHasher::new();
for chunk in &chunks {
hasher.update(chunk);
}
assert_eq!(hasher.finalize(), hash_bytes(&combined));
}
#[test]
fn test_hash_reader() -> Result<()> {
let data = b"streaming reader test";
let hash = hash_reader(&mut &data[..])?;
assert_eq!(hash, hash_bytes(data));
Ok(())
}
}