use sha2::{Digest, Sha256};
use std::fs::{self, File};
use std::io::{BufReader, Read};
use std::path::Path;
use crate::error::{GwmError, Result};
const HASH_BUFFER_SIZE: usize = 8192;
const MAX_CONFIG_SIZE: u64 = 10 * 1024 * 1024;
pub fn compute_file_hash(path: &Path) -> Result<String> {
let metadata = fs::metadata(path)?;
if metadata.len() > MAX_CONFIG_SIZE {
return Err(GwmError::config(format!(
"Config file too large: {} bytes (max: {} bytes)",
metadata.len(),
MAX_CONFIG_SIZE
)));
}
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut hasher = Sha256::new();
let mut buffer = [0u8; HASH_BUFFER_SIZE];
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
let result = hasher.finalize();
Ok(format!("{:x}", result))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_compute_file_hash() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file, "test content").expect("Failed to write to temp file");
let hash = compute_file_hash(file.path()).expect("Hash computation should succeed");
assert_eq!(hash.len(), 64);
let hash2 = compute_file_hash(file.path()).expect("Hash computation should succeed");
assert_eq!(hash, hash2);
}
#[test]
fn test_different_content_different_hash() {
let mut file1 = NamedTempFile::new().expect("Failed to create temp file");
let mut file2 = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file1, "content 1").expect("Failed to write to temp file");
writeln!(file2, "content 2").expect("Failed to write to temp file");
let hash1 = compute_file_hash(file1.path()).expect("Hash computation should succeed");
let hash2 = compute_file_hash(file2.path()).expect("Hash computation should succeed");
assert_ne!(hash1, hash2);
}
#[test]
fn test_nonexistent_file() {
let result = compute_file_hash(Path::new("/nonexistent/file.txt"));
assert!(result.is_err());
}
#[test]
fn test_compute_file_hash_same_content() {
let mut file1 = NamedTempFile::new().expect("Failed to create temp file");
let mut file2 = NamedTempFile::new().expect("Failed to create temp file");
let content = "identical content\nwith multiple lines";
file1
.write_all(content.as_bytes())
.expect("Failed to write to temp file");
file2
.write_all(content.as_bytes())
.expect("Failed to write to temp file");
let hash1 = compute_file_hash(file1.path()).expect("Hash computation should succeed");
let hash2 = compute_file_hash(file2.path()).expect("Hash computation should succeed");
assert_eq!(hash1, hash2);
}
#[test]
fn test_compute_file_hash_empty_file() {
let file = NamedTempFile::new().expect("Failed to create temp file");
let hash = compute_file_hash(file.path()).expect("Hash computation should succeed");
assert_eq!(hash.len(), 64);
assert_eq!(
hash,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn test_compute_file_hash_lowercase_hex() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file, "test").expect("Failed to write to temp file");
let hash = compute_file_hash(file.path()).expect("Hash computation should succeed");
for c in hash.chars() {
assert!(
c.is_ascii_digit() || ('a'..='f').contains(&c),
"Hash should be lowercase hex, got: {}",
c
);
}
}
#[test]
fn test_streaming_hash_matches_direct_hash() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
let content = "test content for streaming verification\n".repeat(100);
file.write_all(content.as_bytes())
.expect("Failed to write to temp file");
let hash = compute_file_hash(file.path()).expect("Hash computation should succeed");
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
let expected = format!("{:x}", hasher.finalize());
assert_eq!(hash, expected);
}
}