use std::fs;
use std::io::Read;
use std::path::Path;
use anyhow::{Context, Result};
use crate::constants::MMAP_THRESHOLD;
const BLAKE3_HEX_LEN: usize = 64;
#[inline]
fn hash_to_hex_string(hash: blake3::Hash) -> String {
let hex = hash.to_hex();
let s = hex.as_str();
debug_assert_eq!(s.len(), BLAKE3_HEX_LEN);
let mut out = String::with_capacity(BLAKE3_HEX_LEN);
out.push_str(s);
out
}
#[inline]
pub fn hash_bytes(data: &[u8]) -> String {
if data.len() as u64 >= MMAP_THRESHOLD {
crate::concurrency::ensure_global_pool();
let mut hasher = blake3::Hasher::new();
hasher.update_rayon(data);
hash_to_hex_string(hasher.finalize())
} else {
hash_to_hex_string(blake3::hash(data))
}
}
pub fn hash_file(path: &Path, max_size: u64) -> Result<String> {
hash_file_with_len(path, max_size).map(|(hash, _len)| hash)
}
pub fn hash_file_with_len(path: &Path, max_size: u64) -> Result<(String, u64)> {
let file = fs::File::open(path)
.inspect_err(|e| tracing::debug!(?e, path = %path.display(), "hash_file: open failed"))
.with_context(|| format!("cannot open {}", path.display()))?;
let metadata = file
.metadata()
.inspect_err(|e| tracing::debug!(?e, path = %path.display(), "hash_file: stat failed"))
.with_context(|| format!("cannot stat {}", path.display()))?;
let len = metadata.len();
if len > max_size {
return Err(crate::error::AtomwriteError::FileTooLarge {
path: path.to_path_buf(),
size: len,
max_size,
}
.into());
}
if len >= MMAP_THRESHOLD {
Ok((hash_file_mmap_rayon(path)?, len))
} else {
Ok((hash_file_read(file, path, len as usize)?, len))
}
}
pub fn hash_file_streaming(path: &Path, max_size: u64) -> Result<(String, u64)> {
let file = fs::File::open(path)
.inspect_err(|e| tracing::debug!(?e, path = %path.display(), "hash streaming: open failed"))
.with_context(|| format!("cannot open {}", path.display()))?;
let len = file
.metadata()
.inspect_err(|e| tracing::debug!(?e, path = %path.display(), "hash streaming: stat failed"))
.with_context(|| format!("cannot stat {}", path.display()))?
.len();
if len > max_size {
return Err(crate::error::AtomwriteError::FileTooLarge {
path: path.to_path_buf(),
size: len,
max_size,
}
.into());
}
let mut reader = std::io::BufReader::with_capacity(crate::constants::BUF_CAPACITY, file);
Ok((hash_reader(&mut reader)?, len))
}
fn hash_file_mmap_rayon(path: &Path) -> Result<String> {
crate::concurrency::ensure_global_pool();
let mut hasher = blake3::Hasher::new();
hasher
.update_mmap_rayon(path)
.with_context(|| format!("cannot mmap-hash {}", path.display()))?;
Ok(hash_to_hex_string(hasher.finalize()))
}
fn hash_file_read(mut file: fs::File, path: &Path, len: usize) -> Result<String> {
use std::io::Read;
let mut data = Vec::new();
if let Err(e) = data.try_reserve_exact(len) {
return Err(crate::error::AtomwriteError::InternalError {
reason: format!(
"allocation failed for {len} bytes hashing {}: {e}",
path.display()
),
}
.into());
}
file.read_to_end(&mut data)
.with_context(|| format!("cannot read {}", path.display()))?;
Ok(hash_bytes(&data))
}
pub fn hash_reader(reader: &mut impl Read) -> Result<String> {
let (hash, _len) = hash_reader_with_len(reader)?;
Ok(hash)
}
pub fn hash_reader_with_len(reader: &mut impl Read) -> Result<(String, u64)> {
let mut hasher = blake3::Hasher::new();
let mut buf = [0u8; crate::constants::BUF_CAPACITY];
let mut total = 0u64;
loop {
let n = reader.read(&mut buf).context("read error during hashing")?;
if n == 0 {
break;
}
total = total.saturating_add(n as u64);
hasher.update(&buf[..n]);
}
Ok((hash_to_hex_string(hasher.finalize()), total))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn large_buffer_hash_matches_blake3_and_builds_the_bounded_pool() {
let data = vec![0xA5u8; MMAP_THRESHOLD as usize + 1];
assert_eq!(hash_bytes(&data), blake3::hash(&data).to_hex().to_string());
let pool = crate::concurrency::configured_pool_size().expect("pool built before rayon use");
assert!(pool >= 1);
}
#[test]
fn large_file_hash_matches_blake3() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("big.bin");
let data = vec![0x5Au8; MMAP_THRESHOLD as usize + 1];
std::fs::write(&path, &data).expect("write");
let got = hash_file(&path, u64::MAX).expect("hash");
assert_eq!(got, blake3::hash(&data).to_hex().to_string());
assert!(crate::concurrency::configured_pool_size().is_some());
}
#[test]
fn streaming_hash_matches_the_buffer_hash() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("s.bin");
let data = vec![0x11u8; 200_000];
std::fs::write(&path, &data).expect("write");
let (streamed, len) = hash_file_streaming(&path, u64::MAX).expect("hash");
assert_eq!(len, data.len() as u64);
assert_eq!(streamed, blake3::hash(&data).to_hex().to_string());
}
#[test]
fn hash_bytes_empty() {
let h = hash_bytes(b"");
assert_eq!(h.len(), 64);
}
#[test]
fn hash_bytes_deterministic() {
let a = hash_bytes(b"hello world");
let b = hash_bytes(b"hello world");
assert_eq!(a, b);
}
#[test]
fn hash_bytes_different_inputs() {
let a = hash_bytes(b"hello");
let b = hash_bytes(b"world");
assert_ne!(a, b);
}
#[test]
fn hash_reader_matches_hash_bytes() {
let data = b"test data for hashing";
let from_bytes = hash_bytes(data);
let mut cursor = Cursor::new(data);
let from_reader = hash_reader(&mut cursor).unwrap();
assert_eq!(from_bytes, from_reader);
}
#[test]
fn hash_file_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
std::fs::write(&path, "file content").unwrap();
let file_hash = hash_file(&path, u64::MAX).unwrap();
let bytes_hash = hash_bytes(b"file content");
assert_eq!(file_hash, bytes_hash);
}
#[test]
fn hash_file_with_len_matches_metadata() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("sized.txt");
let payload = b"twelve bytes";
std::fs::write(&path, payload).unwrap();
let (hash, len) = hash_file_with_len(&path, u64::MAX).unwrap();
assert_eq!(len, payload.len() as u64);
assert_eq!(hash, hash_bytes(payload));
assert_eq!(hash, hash_file(&path, u64::MAX).unwrap());
}
#[test]
fn hash_file_rejects_over_max() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("big.txt");
std::fs::write(&path, vec![b'x'; 64]).unwrap();
let err = hash_file_with_len(&path, 16).unwrap_err();
let ae = err
.downcast_ref::<crate::error::AtomwriteError>()
.expect("AtomwriteError");
assert!(matches!(
ae,
crate::error::AtomwriteError::FileTooLarge {
size: 64,
max_size: 16,
..
}
));
}
}