@@ -12,6 +12,7 @@ use std::fs::{self, File};
1212use std:: io:: { BufReader , BufWriter , Read , Write } ;
1313
1414use crate :: api:: encryption:: CipherHandle ;
15+ use crate :: api:: hashing:: HasherHandle ;
1516use crate :: core:: error:: CryptoError ;
1617use 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
304346use 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) ]
335392mod 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