@@ -99,13 +99,55 @@ impl Digest {
9999 /// the actual hash computation on a background thread in `pool`.
100100 pub async fn reader ( path : PathBuf , pool : & tokio:: runtime:: Handle ) -> Result < String > {
101101 pool. spawn_blocking ( move || {
102- let reader = File :: open ( & path)
103- . with_context ( || format ! ( "Failed to open file for hashing: {:?}" , path) ) ?;
104- Digest :: reader_sync ( reader)
102+ if path. is_dir ( ) {
103+ // For directories (e.g., from proc_macro::tracked::path()),
104+ // recursively hash all file contents in sorted order.
105+ let mut entries = Vec :: new ( ) ;
106+ Self :: collect_dir_entries ( & path, & mut entries) ?;
107+ entries. sort ( ) ;
108+ let mut digest = Digest :: new ( ) ;
109+ for entry in & entries {
110+ // Hash the relative path as a delimiter
111+ let rel = entry. strip_prefix ( & path) . unwrap_or ( entry) ;
112+ digest. update ( rel. to_string_lossy ( ) . as_bytes ( ) ) ;
113+ digest. update ( b"\0 " ) ;
114+ let mut file = File :: open ( entry)
115+ . with_context ( || format ! ( "Failed to open file for hashing: {:?}" , entry) ) ?;
116+ let mut buf = vec ! [ 0u8 ; HASH_BUFFER_SIZE ] ;
117+ loop {
118+ let n = file. read ( & mut buf) ?;
119+ if n == 0 {
120+ break ;
121+ }
122+ digest. update ( & buf[ ..n] ) ;
123+ }
124+ }
125+ Ok ( digest. finish ( ) )
126+ } else {
127+ let reader = File :: open ( & path)
128+ . with_context ( || format ! ( "Failed to open file for hashing: {:?}" , path) ) ?;
129+ Digest :: reader_sync ( reader)
130+ }
105131 } )
106132 . await ?
107133 }
108134
135+ /// Recursively collect all file paths within a directory.
136+ fn collect_dir_entries ( dir : & Path , entries : & mut Vec < PathBuf > ) -> Result < ( ) > {
137+ for entry in std:: fs:: read_dir ( dir)
138+ . with_context ( || format ! ( "Failed to read directory for hashing: {:?}" , dir) ) ?
139+ {
140+ let entry = entry?;
141+ let path = entry. path ( ) ;
142+ if path. is_dir ( ) {
143+ Self :: collect_dir_entries ( & path, entries) ?;
144+ } else {
145+ entries. push ( path) ;
146+ }
147+ }
148+ Ok ( ( ) )
149+ }
150+
109151 pub fn update ( & mut self , bytes : & [ u8 ] ) {
110152 self . inner . update ( bytes) ;
111153 }
@@ -1248,7 +1290,7 @@ pub fn resolve_compiler_avoiding_ccache(
12481290
12491291#[ cfg( test) ]
12501292mod tests {
1251- use super :: { OsStrExt , TimeMacroFinder , resolve_compiler_avoiding_ccache} ;
1293+ use super :: { Digest , OsStrExt , TimeMacroFinder , resolve_compiler_avoiding_ccache} ;
12521294 use std:: ffi:: { OsStr , OsString } ;
12531295 use std:: path:: Path ;
12541296
@@ -1705,4 +1747,91 @@ mod tests {
17051747 let normalized = super :: normalize_win_path ( input) ;
17061748 assert_eq ! ( normalized, b"" ) ;
17071749 }
1750+
1751+ #[ tokio:: test]
1752+ async fn test_digest_reader_hashes_file ( ) {
1753+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
1754+ let path = temp. path ( ) . join ( "hello.txt" ) ;
1755+ std:: fs:: write ( & path, b"hello, world" ) . unwrap ( ) ;
1756+ let pool = tokio:: runtime:: Handle :: current ( ) ;
1757+ let hash = Digest :: reader ( path, & pool) . await . unwrap ( ) ;
1758+ assert_eq ! ( hash. len( ) , 64 , "blake3 hex digest is 64 chars" ) ;
1759+ }
1760+
1761+ // Regression for https://github.com/mozilla/sccache/issues/2653: rustc's
1762+ // dep-info may list a directory path (e.g. from a proc macro that calls
1763+ // `proc_macro::tracked_path::path()` on a directory). The four tests below
1764+ // exercise Digest::reader against directories.
1765+
1766+ #[ tokio:: test]
1767+ async fn test_digest_reader_hashes_directory ( ) {
1768+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
1769+ let dir = temp. path ( ) ;
1770+ std:: fs:: create_dir_all ( dir. join ( "nested/deeper" ) ) . unwrap ( ) ;
1771+ std:: fs:: write ( dir. join ( "root.txt" ) , b"root" ) . unwrap ( ) ;
1772+ std:: fs:: write ( dir. join ( "nested/inner.txt" ) , b"inner" ) . unwrap ( ) ;
1773+ std:: fs:: write ( dir. join ( "nested/deeper/deep.txt" ) , b"deep" ) . unwrap ( ) ;
1774+
1775+ let pool = tokio:: runtime:: Handle :: current ( ) ;
1776+ let hash = Digest :: reader ( dir. to_path_buf ( ) , & pool) . await . unwrap ( ) ;
1777+ assert_eq ! ( hash. len( ) , 64 ) ;
1778+ }
1779+
1780+ #[ tokio:: test]
1781+ async fn test_digest_reader_directory_is_deterministic ( ) {
1782+ fn populate ( dir : & std:: path:: Path ) {
1783+ std:: fs:: create_dir_all ( dir. join ( "a/b" ) ) . unwrap ( ) ;
1784+ std:: fs:: create_dir_all ( dir. join ( "c" ) ) . unwrap ( ) ;
1785+ std:: fs:: write ( dir. join ( "a/b/one.txt" ) , b"one" ) . unwrap ( ) ;
1786+ std:: fs:: write ( dir. join ( "a/two.txt" ) , b"two" ) . unwrap ( ) ;
1787+ std:: fs:: write ( dir. join ( "c/three.txt" ) , b"three" ) . unwrap ( ) ;
1788+ std:: fs:: write ( dir. join ( "four.txt" ) , b"four" ) . unwrap ( ) ;
1789+ }
1790+ let t1 = tempfile:: tempdir ( ) . unwrap ( ) ;
1791+ let t2 = tempfile:: tempdir ( ) . unwrap ( ) ;
1792+ populate ( t1. path ( ) ) ;
1793+ populate ( t2. path ( ) ) ;
1794+ let pool = tokio:: runtime:: Handle :: current ( ) ;
1795+ let h1 = Digest :: reader ( t1. path ( ) . to_path_buf ( ) , & pool)
1796+ . await
1797+ . unwrap ( ) ;
1798+ let h2 = Digest :: reader ( t2. path ( ) . to_path_buf ( ) , & pool)
1799+ . await
1800+ . unwrap ( ) ;
1801+ assert_eq ! ( h1, h2) ;
1802+ }
1803+
1804+ #[ tokio:: test]
1805+ async fn test_digest_reader_directory_detects_content_change ( ) {
1806+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
1807+ let dir = temp. path ( ) ;
1808+ std:: fs:: create_dir_all ( dir. join ( "sub" ) ) . unwrap ( ) ;
1809+ std:: fs:: write ( dir. join ( "sub/data.txt" ) , b"before" ) . unwrap ( ) ;
1810+ let pool = tokio:: runtime:: Handle :: current ( ) ;
1811+ let before = Digest :: reader ( dir. to_path_buf ( ) , & pool) . await . unwrap ( ) ;
1812+ std:: fs:: write ( dir. join ( "sub/data.txt" ) , b"after" ) . unwrap ( ) ;
1813+ let after = Digest :: reader ( dir. to_path_buf ( ) , & pool) . await . unwrap ( ) ;
1814+ assert_ne ! ( before, after) ;
1815+ }
1816+
1817+ #[ tokio:: test]
1818+ async fn test_digest_reader_directory_detects_path_change ( ) {
1819+ // Two dirs with identical file *contents* but different file *names*
1820+ // must hash to different values, because collect_dir_entries mixes
1821+ // the relative path into the digest as a delimiter.
1822+ let t1 = tempfile:: tempdir ( ) . unwrap ( ) ;
1823+ let t2 = tempfile:: tempdir ( ) . unwrap ( ) ;
1824+ std:: fs:: create_dir_all ( t1. path ( ) . join ( "sub" ) ) . unwrap ( ) ;
1825+ std:: fs:: create_dir_all ( t2. path ( ) . join ( "sub" ) ) . unwrap ( ) ;
1826+ std:: fs:: write ( t1. path ( ) . join ( "sub/a.txt" ) , b"x" ) . unwrap ( ) ;
1827+ std:: fs:: write ( t2. path ( ) . join ( "sub/b.txt" ) , b"x" ) . unwrap ( ) ;
1828+ let pool = tokio:: runtime:: Handle :: current ( ) ;
1829+ let h1 = Digest :: reader ( t1. path ( ) . to_path_buf ( ) , & pool)
1830+ . await
1831+ . unwrap ( ) ;
1832+ let h2 = Digest :: reader ( t2. path ( ) . to_path_buf ( ) , & pool)
1833+ . await
1834+ . unwrap ( ) ;
1835+ assert_ne ! ( h1, h2) ;
1836+ }
17081837}
0 commit comments