Skip to content

Commit 5e52a47

Browse files
authored
Merge pull request #46 from MicroClub-USTHB/adelayoub/streaming-hash
feat(streaming): add streaming file hash with BLAKE3/SHA-3
2 parents 0cb3318 + 4f8c5af commit 5e52a47

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

rust/src/api/hashing/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,30 @@ impl HasherHandle {
2323
inner: Mutex::new(hasher),
2424
}
2525
}
26+
27+
pub(crate) fn update_raw(&self, data: &[u8]) -> Result<(), CryptoError> {
28+
let mut guard = self
29+
.inner
30+
.lock()
31+
.map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?;
32+
guard.update(data)
33+
}
34+
35+
pub(crate) fn finalize_raw(&self) -> Result<Vec<u8>, CryptoError> {
36+
let guard = self
37+
.inner
38+
.lock()
39+
.map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?;
40+
guard.finalize()
41+
}
42+
43+
pub(crate) fn reset_raw(&self) -> Result<(), CryptoError> {
44+
let mut guard = self
45+
.inner
46+
.lock()
47+
.map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?;
48+
guard.reset()
49+
}
2650
}
2751

2852
/// Create a BLAKE3 hasher handle.

rust/src/api/streaming.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::fs::{self, File};
1212
use std::io::{BufReader, BufWriter, Read, Write};
1313

1414
use crate::api::encryption::CipherHandle;
15+
use crate::api::hashing::HasherHandle;
1516
use crate::core::error::CryptoError;
1617
use crate::core::streaming::{
1718
finish_file, pad_last_chunk, strip_last_chunk_padding, ChunkAad, ChunkReader, ChunkWriter,
@@ -299,6 +300,47 @@ pub(crate) fn decrypt_file_impl(
299300
Ok(())
300301
}
301302

303+
// -- Streaming hash (no encryption padding) -----------------------------------
304+
305+
/// Hash a file in streaming 64KB chunks.
306+
///
307+
/// Resets the hasher first to ensure a clean state, then feeds raw file bytes
308+
/// (no padding). The digest matches `blake3_hash(fs::read(path))`.
309+
pub(crate) fn hash_file_impl(
310+
hasher: &HasherHandle,
311+
file_path: &str,
312+
on_progress: &dyn Fn(f64),
313+
) -> Result<Vec<u8>, CryptoError> {
314+
hasher.reset_raw()?;
315+
316+
let file = File::open(file_path)
317+
.map_err(|e| CryptoError::IoError(format!("Cannot open input '{file_path}': {e}")))?;
318+
let file_size = file
319+
.metadata()
320+
.map_err(|e| CryptoError::IoError(format!("Cannot stat input: {e}")))?
321+
.len();
322+
323+
let mut reader = BufReader::new(file);
324+
let mut buf = vec![0u8; CHUNK_SIZE];
325+
let mut bytes_hashed: u64 = 0;
326+
327+
loop {
328+
let n = read_full(&mut reader, &mut buf)?;
329+
if n == 0 {
330+
break;
331+
}
332+
hasher.update_raw(&buf[..n])?;
333+
bytes_hashed += n as u64;
334+
if file_size > 0 {
335+
on_progress((bytes_hashed as f64 / file_size as f64).min(0.99));
336+
}
337+
}
338+
339+
let digest = hasher.finalize_raw()?;
340+
on_progress(1.0);
341+
Ok(digest)
342+
}
343+
302344
// -- FRB entry points (thin wrappers) ----------------------------------------
303345

304346
use crate::frb_generated::StreamSink;
@@ -331,6 +373,21 @@ pub fn stream_decrypt_file(
331373
})
332374
}
333375

376+
/// Hash a file using streaming 64KB chunks.
377+
///
378+
/// Reads raw file bytes (no encryption padding) so the digest matches
379+
/// one-shot `blake3_hash()` / `sha3_hash()` output.
380+
/// Progress (0.0..1.0) is pushed to `progress_sink`.
381+
pub fn stream_hash_file(
382+
hasher: &HasherHandle,
383+
file_path: String,
384+
progress_sink: StreamSink<f64>,
385+
) -> Result<Vec<u8>, CryptoError> {
386+
hash_file_impl(hasher, &file_path, &|p| {
387+
let _ = progress_sink.add(p);
388+
})
389+
}
390+
334391
#[cfg(test)]
335392
mod tests {
336393
use super::*;
@@ -725,4 +782,97 @@ mod tests {
725782
let tmp = format!("{}.tmp", encrypted.to_str().expect("p"));
726783
assert!(!std::path::Path::new(&tmp).exists(), "Temp file should be gone after success");
727784
}
785+
786+
// -- Streaming hash tests -------------------------------------------------
787+
788+
fn make_blake3_hasher() -> HasherHandle {
789+
crate::api::hashing::create_blake3()
790+
}
791+
792+
fn make_sha3_hasher() -> HasherHandle {
793+
crate::api::hashing::create_sha3()
794+
}
795+
796+
#[test]
797+
fn test_streaming_hash_matches_oneshot_blake3() {
798+
let data = b"Hello, streaming hash with BLAKE3!";
799+
let dir = tempfile::tempdir().expect("tmpdir");
800+
let path = dir.path().join("input.bin");
801+
fs::write(&path, data).expect("write");
802+
803+
let hasher = make_blake3_hasher();
804+
let digest = hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress)
805+
.expect("hash");
806+
807+
let oneshot = crate::api::hashing::blake3_hash(data.to_vec());
808+
assert_eq!(digest, oneshot);
809+
}
810+
811+
#[test]
812+
fn test_streaming_hash_matches_oneshot_sha3() {
813+
let data = b"Hello, streaming hash with SHA-3!";
814+
let dir = tempfile::tempdir().expect("tmpdir");
815+
let path = dir.path().join("input.bin");
816+
fs::write(&path, data).expect("write");
817+
818+
let hasher = make_sha3_hasher();
819+
let digest = hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress)
820+
.expect("hash");
821+
822+
let oneshot = crate::api::hashing::sha3_hash(data.to_vec());
823+
assert_eq!(digest, oneshot);
824+
}
825+
826+
#[test]
827+
fn test_streaming_hash_empty_file() {
828+
let dir = tempfile::tempdir().expect("tmpdir");
829+
let path = dir.path().join("empty.bin");
830+
fs::write(&path, b"").expect("write");
831+
832+
let hasher = make_blake3_hasher();
833+
let digest = hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress)
834+
.expect("hash");
835+
836+
let oneshot = crate::api::hashing::blake3_hash(Vec::new());
837+
assert_eq!(digest, oneshot);
838+
}
839+
840+
#[test]
841+
fn test_streaming_hash_large_file() {
842+
let data = vec![0xAB; 1024 * 1024 + 37]; // 1MB + 37 bytes
843+
let dir = tempfile::tempdir().expect("tmpdir");
844+
let path = dir.path().join("large.bin");
845+
fs::write(&path, &data).expect("write");
846+
847+
let hasher = make_blake3_hasher();
848+
let digest = hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress)
849+
.expect("hash");
850+
851+
let oneshot = crate::api::hashing::blake3_hash(data);
852+
assert_eq!(digest, oneshot);
853+
}
854+
855+
#[test]
856+
fn test_streaming_hash_exact_boundary() {
857+
let data = vec![0xCD; CHUNK_SIZE]; // exactly 64KB
858+
let dir = tempfile::tempdir().expect("tmpdir");
859+
let path = dir.path().join("boundary.bin");
860+
fs::write(&path, &data).expect("write");
861+
862+
let hasher = make_blake3_hasher();
863+
let progress = std::sync::Mutex::new(Vec::new());
864+
let digest = hash_file_impl(
865+
&hasher,
866+
path.to_str().expect("p"),
867+
&|p| progress.lock().expect("lock").push(p),
868+
)
869+
.expect("hash");
870+
871+
let oneshot = crate::api::hashing::blake3_hash(data);
872+
assert_eq!(digest, oneshot);
873+
874+
let vals = progress.lock().expect("lock");
875+
assert!(!vals.is_empty());
876+
assert!((vals.last().copied().unwrap_or(0.0) - 1.0).abs() < f64::EPSILON);
877+
}
728878
}

0 commit comments

Comments
 (0)