44//! deterministic serialization across languages and platforms, then applies
55//! xxHash64 for fast hashing.
66
7+ mod oid_hash;
78mod traits;
89
910use std:: { collections:: HashMap , sync:: Arc } ;
1011
1112use capnp:: message:: { Builder , HeapAllocator } ;
13+ pub use oid_hash:: OidHash ;
1214pub use traits:: TurboHash ;
1315// Re-export for backward compatibility. New code should import from `turborepo_types`.
1416#[ deprecated(
@@ -108,7 +110,7 @@ pub struct LockFilePackages(pub Vec<turborepo_lockfiles::Package>);
108110pub struct LockFilePackagesRef < ' a > ( pub Vec < & ' a turborepo_lockfiles:: Package > ) ;
109111
110112#[ derive( Debug , Clone ) ]
111- pub struct FileHashes ( pub Vec < ( turbopath:: RelativeUnixPathBuf , String ) > ) ;
113+ pub struct FileHashes ( pub Vec < ( turbopath:: RelativeUnixPathBuf , OidHash ) > ) ;
112114
113115/// Wrapper type for TaskOutputs to enable capnp serialization.
114116/// This is needed due to Rust's orphan rule - we can't implement From
@@ -244,7 +246,7 @@ impl From<FileHashes> for Builder<HeapAllocator> {
244246 for ( i, ( key, value) ) in file_hashes. iter ( ) . enumerate ( ) {
245247 let mut entry = entries. reborrow ( ) . get ( i as u32 ) ;
246248 entry. set_key ( key. as_str ( ) ) ;
247- entry. set_value ( value) ;
249+ entry. set_value ( & * * value) ;
248250 }
249251 }
250252
@@ -286,7 +288,7 @@ impl From<&FileHashes> for Builder<HeapAllocator> {
286288 for ( i, ( key, value) ) in file_hashes. iter ( ) . enumerate ( ) {
287289 let mut entry = entries. reborrow ( ) . get ( i as u32 ) ;
288290 entry. set_key ( key. as_str ( ) ) ;
289- entry. set_value ( value) ;
291+ entry. set_value ( & * * value) ;
290292 }
291293 }
292294
@@ -504,7 +506,8 @@ mod test {
504506 use turborepo_types:: { EnvMode , TaskOutputs } ;
505507
506508 use super :: {
507- FileHashes , GlobalHashable , LockFilePackages , LockFilePackagesRef , TaskHashable , TurboHash ,
509+ FileHashes , GlobalHashable , LockFilePackages , LockFilePackagesRef , OidHash , TaskHashable ,
510+ TurboHash ,
508511 } ;
509512
510513 #[ test]
@@ -623,39 +626,46 @@ mod test {
623626 lock_file_packages ( packages. collect ( ) , "4fd770c37194168e" ) ;
624627 }
625628
626- fn sorted_file_hashes ( pairs : Vec < ( String , String ) > ) -> FileHashes {
627- let mut v: Vec < _ > = pairs
629+ fn sorted_file_hashes ( pairs : Vec < ( & str , & str ) > ) -> FileHashes {
630+ let mut v: Vec < ( turbopath :: RelativeUnixPathBuf , OidHash ) > = pairs
628631 . into_iter ( )
629- . map ( |( a, b) | ( turbopath:: RelativeUnixPathBuf :: new ( a) . unwrap ( ) , b) )
632+ . map ( |( a, b) | {
633+ (
634+ turbopath:: RelativeUnixPathBuf :: new ( a) . unwrap ( ) ,
635+ OidHash :: from_hex_str ( b) ,
636+ )
637+ } )
630638 . collect ( ) ;
631639 v. sort_by ( |( a, _) , ( b, _) | a. cmp ( b) ) ;
632640 FileHashes ( v)
633641 }
634642
643+ // OID-sized test hashes (40 hex chars each)
644+ const OID_A : & str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ;
645+ const OID_B : & str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ;
646+ const OID_C : & str = "cccccccccccccccccccccccccccccccccccccccc" ;
647+ const OID_D : & str = "dddddddddddddddddddddddddddddddddddddddd" ;
648+
635649 #[ test_case( vec![ ] , "459c029558afe716" ; "empty" ) ]
636650 #[ test_case( vec![
637- ( "a" . to_string ( ) , "b" . to_string ( ) ) ,
638- ( "c" . to_string ( ) , "d" . to_string ( ) ) ,
639- ] , "c9301c0bf1899c07 " ; "non-empty" ) ]
651+ ( "a" , OID_A ) ,
652+ ( "c" , OID_B ) ,
653+ ] , "03e24e42bd35dcaf " ; "non-empty" ) ]
640654 #[ test_case( vec![
641- ( "c" . to_string ( ) , "d" . to_string ( ) ) ,
642- ( "a" . to_string ( ) , "b" . to_string ( ) ) ,
643- ] , "c9301c0bf1899c07 " ; "order resistant" ) ]
644- fn file_hashes ( pairs : Vec < ( String , String ) > , expected : & str ) {
655+ ( "c" , OID_B ) ,
656+ ( "a" , OID_A ) ,
657+ ] , "03e24e42bd35dcaf " ; "order resistant" ) ]
658+ fn file_hashes ( pairs : Vec < ( & str , & str ) > , expected : & str ) {
645659 assert_eq ! ( sorted_file_hashes( pairs) . hash( ) , expected) ;
646660 }
647661
648662 #[ test]
649663 fn file_hashes_ref_matches_owned ( ) {
650- let file_hashes = sorted_file_hashes ( vec ! [
651- ( "c" . to_string( ) , "d" . to_string( ) ) ,
652- ( "a" . to_string( ) , "b" . to_string( ) ) ,
653- ] ) ;
664+ let file_hashes = sorted_file_hashes ( vec ! [ ( "c" , OID_B ) , ( "a" , OID_A ) ] ) ;
654665
655666 let ref_hash = ( & file_hashes) . hash ( ) ;
656667 let owned_hash = file_hashes. hash ( ) ;
657668 assert_eq ! ( ref_hash, owned_hash) ;
658- assert_eq ! ( ref_hash, "c9301c0bf1899c07" ) ;
659669 }
660670
661671 #[ test]
@@ -668,7 +678,7 @@ mod test {
668678 (
669679 turbopath:: RelativeUnixPathBuf :: new ( format ! ( "path/to/file_{i:03}" ) )
670680 . unwrap ( ) ,
671- format ! ( "hash_{i}" ) ,
681+ OidHash :: from_hex_str ( & format ! ( "{i:040x}" ) ) ,
672682 )
673683 } )
674684 . collect ( ) ,
@@ -717,58 +727,49 @@ mod test {
717727 }
718728
719729 // Regression: FileHashes constructed from a pre-sorted Vec must produce
720- // identical hashes to FileHashes constructed from a HashMap. This captures
721- // the invariant that must hold when switching FileHashes from HashMap to
722- // sorted Vec.
723- // Regression: sorted Vec construction must produce identical hashes to what
724- // the old HashMap-based construction produced. Pinned hash values.
730+ // identical hashes regardless of input order.
725731 #[ test]
726732 fn file_hashes_sorted_vec_pinned_values ( ) {
727- let pairs = [
728- ( "c/z.ts" , "hash_cz" ) ,
729- ( "a/b.ts" , "hash_ab" ) ,
730- ( "a/a.ts" , "hash_aa" ) ,
731- ( "b.ts" , "hash_b" ) ,
733+ let pairs = vec ! [
734+ ( "c/z.ts" , OID_C ) ,
735+ ( "a/b.ts" , OID_A ) ,
736+ ( "a/a.ts" , OID_B ) ,
737+ ( "b.ts" , OID_D ) ,
732738 ] ;
733739
734- let fh = sorted_file_hashes (
735- pairs
736- . iter ( )
737- . map ( |( p, h) | ( p. to_string ( ) , h. to_string ( ) ) )
738- . collect ( ) ,
739- ) ;
740+ let fh = sorted_file_hashes ( pairs. clone ( ) ) ;
740741 let hash = fh. hash ( ) ;
741742
742743 // Verify ref and owned produce same hash
743- let fh2 = sorted_file_hashes (
744- pairs
745- . iter ( )
746- . map ( |( p, h) | ( p. to_string ( ) , h. to_string ( ) ) )
747- . collect ( ) ,
748- ) ;
744+ let fh2 = sorted_file_hashes ( pairs) ;
749745 assert_eq ! ( ( & fh2) . hash( ) , hash) ;
750746 }
751747
752748 // Regression: large FileHashes must produce deterministic hashes regardless
753749 // of original insertion order.
754750 #[ test]
755751 fn file_hashes_large_deterministic ( ) {
756- let fh_forward = sorted_file_hashes (
757- ( 0 ..1000 )
758- . map ( |i| ( format ! ( "pkg/file_{:04}" , i) , format ! ( "{:040x}" , i) ) )
759- . collect ( ) ,
760- ) ;
752+ let pairs_forward: Vec < _ > = ( 0 ..1000 )
753+ . map ( |i| {
754+ // Leak to get &'static str for the test helper
755+ let path: & str = Box :: leak ( format ! ( "pkg/file_{i:04}" ) . into_boxed_str ( ) ) ;
756+ let hash: & str = Box :: leak ( format ! ( "{i:040x}" ) . into_boxed_str ( ) ) ;
757+ ( path, hash)
758+ } )
759+ . collect ( ) ;
761760
762- let fh_reverse = sorted_file_hashes (
763- ( 0 ..1000 )
764- . rev ( )
765- . map ( |i| ( format ! ( "pkg/file_{:04}" , i) , format ! ( "{:040x}" , i) ) )
766- . collect ( ) ,
767- ) ;
761+ let pairs_reverse: Vec < _ > = ( 0 ..1000 )
762+ . rev ( )
763+ . map ( |i| {
764+ let path: & str = Box :: leak ( format ! ( "pkg/file_{i:04}" ) . into_boxed_str ( ) ) ;
765+ let hash: & str = Box :: leak ( format ! ( "{i:040x}" ) . into_boxed_str ( ) ) ;
766+ ( path, hash)
767+ } )
768+ . collect ( ) ;
768769
769770 assert_eq ! (
770- fh_forward . hash( ) ,
771- fh_reverse . hash( ) ,
771+ sorted_file_hashes ( pairs_forward ) . hash( ) ,
772+ sorted_file_hashes ( pairs_reverse ) . hash( ) ,
772773 "insertion order must not affect hash output"
773774 ) ;
774775 }
0 commit comments