@@ -7,6 +7,8 @@ use rspack_fs::{FileMetadata, ReadableFileSystem};
77use rspack_paths:: { ArcPath , ArcPathDashMap , AssertUtf8 } ;
88use rustc_hash:: FxHasher ;
99
10+ use super :: { PackageHelper , SnapshotOptions } ;
11+
1012/// Content hash with modification time.
1113#[ derive( Debug , Clone , Default ) ]
1214pub struct ContentHash {
@@ -17,26 +19,31 @@ pub struct ContentHash {
1719/// A helper for computing content hashes of files and directories.
1820#[ derive( Debug ) ]
1921pub struct HashHelper {
20- /// File system abstraction for reading file contents.
2122 fs : Arc < dyn ReadableFileSystem > ,
22-
23- /// Cache for file content hashes.
23+ snapshot_options : Arc < SnapshotOptions > ,
24+ package_helper : Arc < PackageHelper > ,
2425 file_cache : ArcPathDashMap < Option < ContentHash > > ,
25- /// Cache for directory content hashes.
2626 dir_cache : ArcPathDashMap < Option < ContentHash > > ,
2727}
2828
2929impl HashHelper {
3030 /// Creates a new HashHelper instance with the given file system.
31- pub fn new ( fs : Arc < dyn ReadableFileSystem > ) -> Self {
31+ pub fn new (
32+ fs : Arc < dyn ReadableFileSystem > ,
33+ snapshot_options : Arc < SnapshotOptions > ,
34+ package_helper : Arc < PackageHelper > ,
35+ ) -> Self {
3236 Self {
3337 fs,
38+ snapshot_options,
39+ package_helper,
3440 file_cache : Default :: default ( ) ,
3541 dir_cache : Default :: default ( ) ,
3642 }
3743 }
3844
39- /// Calculate file hash, return default for non-files.
45+ /// Computes content hash for a file.
46+ /// Returns None if the file does not exist.
4047 async fn inner_file_hash (
4148 & self ,
4249 path : & ArcPath ,
@@ -57,28 +64,26 @@ impl HashHelper {
5764 metadata
5865 } ;
5966
60- let hash = if metadata. is_file && !metadata. is_symlink {
61- if let Ok ( content) = self . fs . read ( utf8_path) . await {
62- // mtime is the larger of ctime and mtime
63- let mtime = if metadata. ctime_ms > metadata. mtime_ms {
64- metadata. ctime_ms
65- } else {
66- metadata. mtime_ms
67- } ;
68- let mut hasher = FxHasher :: default ( ) ;
69- content. hash ( & mut hasher) ;
70- Some ( ContentHash {
71- hash : hasher. finish ( ) ,
72- mtime,
73- } )
74- } else {
75- None
76- }
67+ // mtime is the larger of ctime and mtime
68+ let mtime = if metadata. ctime_ms > metadata. mtime_ms {
69+ metadata. ctime_ms
7770 } else {
78- // directory & symlink
79- Some ( ContentHash :: default ( ) )
71+ metadata. mtime_ms
8072 } ;
81-
73+ let mut hasher = FxHasher :: default ( ) ;
74+ if metadata. is_symlink {
75+ if let Ok ( target) = self . fs . canonicalize ( utf8_path) . await {
76+ target. hash ( & mut hasher)
77+ }
78+ } else if metadata. is_file
79+ && let Ok ( content) = self . fs . read ( utf8_path) . await
80+ {
81+ content. hash ( & mut hasher) ;
82+ } ;
83+ let hash = Some ( ContentHash {
84+ hash : hasher. finish ( ) ,
85+ mtime,
86+ } ) ;
8287 self . file_cache . insert ( path. into ( ) , hash. clone ( ) ) ;
8388 hash
8489 }
@@ -103,10 +108,21 @@ impl HashHelper {
103108
104109 let hash = if metadata. is_directory && !metadata. is_symlink {
105110 if let Ok ( mut children) = self . fs . read_dir ( utf8_path) . await {
106- children. sort ( ) ;
107111 let mut hasher = FxHasher :: default ( ) ;
112+ children. sort ( ) ;
108113 for item in children {
109114 let child_path = ArcPath :: from ( path. join ( item) ) ;
115+ let child_path_str = child_path. to_string_lossy ( ) ;
116+ if self . snapshot_options . is_immutable_path ( & child_path_str) {
117+ continue ;
118+ }
119+ if self . snapshot_options . is_managed_path ( & child_path_str) {
120+ if let Some ( version) = self . package_helper . package_version ( & child_path) . await {
121+ version. hash ( & mut hasher) ;
122+ }
123+ continue ;
124+ }
125+
110126 if let Some ( ContentHash { hash, .. } ) = self . dir_hash ( & child_path) . await {
111127 hash. hash ( & mut hasher) ;
112128 }
@@ -134,40 +150,57 @@ mod tests {
134150 use rspack_fs:: { MemoryFileSystem , WritableFileSystem } ;
135151 use rspack_paths:: ArcPath ;
136152
137- use super :: HashHelper ;
153+ use super :: {
154+ super :: super :: super :: snapshot:: PathMatcher , HashHelper , PackageHelper , SnapshotOptions ,
155+ } ;
156+
157+ fn new_helper ( fs : Arc < MemoryFileSystem > ) -> HashHelper {
158+ HashHelper :: new (
159+ fs. clone ( ) ,
160+ Arc :: new ( SnapshotOptions :: new (
161+ vec ! [ PathMatcher :: String ( "immutable" . into( ) ) ] ,
162+ vec ! [ ] ,
163+ vec ! [ PathMatcher :: String ( "node_modules" . into( ) ) ] ,
164+ ) ) ,
165+ Arc :: new ( PackageHelper :: new ( fs) ) ,
166+ )
167+ }
138168
139169 #[ tokio:: test]
140170 async fn file_hash ( ) {
141171 let fs = Arc :: new ( MemoryFileSystem :: default ( ) ) ;
142172 fs. create_dir_all ( "/" . into ( ) ) . await . unwrap ( ) ;
143173 fs. write ( "/hash.js" . into ( ) , "abc" . as_bytes ( ) ) . await . unwrap ( ) ;
144174
145- let helper = HashHelper :: new ( fs. clone ( ) ) ;
175+ let helper = new_helper ( fs. clone ( ) ) ;
146176 assert ! (
147177 helper
148178 . file_hash( & ArcPath :: from( "/not_exist.js" ) )
149179 . await
150180 . is_none( )
151181 ) ;
182+ // check directory
152183 let hash0 = helper. file_hash ( & ArcPath :: from ( "/" ) ) . await . unwrap ( ) ;
153184 assert_eq ! ( hash0. hash, 0 ) ;
154- assert_eq ! ( hash0. mtime, 0 ) ;
155185
156186 let hash1 = helper. file_hash ( & ArcPath :: from ( "/hash.js" ) ) . await . unwrap ( ) ;
157187
158- helper. file_cache . clear ( ) ;
159188 std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
189+ // do nothing
190+ let helper = new_helper ( fs. clone ( ) ) ;
160191 let hash2 = helper. file_hash ( & ArcPath :: from ( "/hash.js" ) ) . await . unwrap ( ) ;
161192 assert_eq ! ( hash1. hash, hash2. hash) ;
162193 assert_eq ! ( hash1. mtime, hash2. mtime) ;
163194
164- helper. file_cache . clear ( ) ;
195+ // same content
196+ let helper = new_helper ( fs. clone ( ) ) ;
165197 fs. write ( "/hash.js" . into ( ) , "abc" . as_bytes ( ) ) . await . unwrap ( ) ;
166198 let hash3 = helper. file_hash ( & ArcPath :: from ( "/hash.js" ) ) . await . unwrap ( ) ;
167199 assert_eq ! ( hash1. hash, hash3. hash) ;
168200 assert ! ( hash1. mtime < hash3. mtime) ;
169201
170- helper. file_cache . clear ( ) ;
202+ // diff content
203+ let helper = new_helper ( fs. clone ( ) ) ;
171204 fs. write ( "/hash.js" . into ( ) , "abcd" . as_bytes ( ) )
172205 . await
173206 . unwrap ( ) ;
@@ -180,35 +213,106 @@ mod tests {
180213 async fn dir_hash ( ) {
181214 let fs = Arc :: new ( MemoryFileSystem :: default ( ) ) ;
182215 fs. create_dir_all ( "/a" . into ( ) ) . await . unwrap ( ) ;
216+ fs. create_dir_all ( "/node_modules/lib" . into ( ) ) . await . unwrap ( ) ;
183217 fs. write ( "/a/a1.js" . into ( ) , "a1" . as_bytes ( ) ) . await . unwrap ( ) ;
184218 fs. write ( "/a/a2.js" . into ( ) , "a2" . as_bytes ( ) ) . await . unwrap ( ) ;
185219 fs. write ( "/b.js" . into ( ) , "b" . as_bytes ( ) ) . await . unwrap ( ) ;
220+ fs. write ( "/immutable.js" . into ( ) , "immut" . as_bytes ( ) )
221+ . await
222+ . unwrap ( ) ;
223+ fs. write (
224+ "/node_modules/lib/index.js" . into ( ) ,
225+ "const a = 1" . as_bytes ( ) ,
226+ )
227+ . await
228+ . unwrap ( ) ;
229+ fs. write (
230+ "/node_modules/lib/package.json" . into ( ) ,
231+ r#"{"version": "0.0.1"}"# . as_bytes ( ) ,
232+ )
233+ . await
234+ . unwrap ( ) ;
186235
187- let helper = HashHelper :: new ( fs. clone ( ) ) ;
188-
236+ let helper = new_helper ( fs. clone ( ) ) ;
189237 let hash1 = helper. dir_hash ( & ArcPath :: from ( "/" ) ) . await . unwrap ( ) ;
238+ assert_eq ! ( hash1. mtime, 0 ) ;
190239
191- helper. file_cache . clear ( ) ;
192- helper. dir_cache . clear ( ) ;
193240 std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
241+
242+ // do nothing
243+ let helper = new_helper ( fs. clone ( ) ) ;
194244 let hash2 = helper. dir_hash ( & ArcPath :: from ( "/" ) ) . await . unwrap ( ) ;
195245 assert_eq ! ( hash1. hash, hash2. hash) ;
196- assert_eq ! ( hash1. mtime, 0 ) ;
197246 assert_eq ! ( hash2. mtime, 0 ) ;
198247
199- helper. file_cache . clear ( ) ;
200- helper. dir_cache . clear ( ) ;
201248 std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
249+
250+ // do something will not update hash
251+ let helper = new_helper ( fs. clone ( ) ) ;
252+ // write same content
202253 fs. write ( "/a/a2.js" . into ( ) , "a2" . as_bytes ( ) ) . await . unwrap ( ) ;
254+ // edit immutable file
255+ fs. write ( "/immutable.js" . into ( ) , "next" . as_bytes ( ) )
256+ . await
257+ . unwrap ( ) ;
258+ // edit node_modules file
259+ fs. write (
260+ "/node_modules/lib/index.js" . into ( ) ,
261+ "const a = 2" . as_bytes ( ) ,
262+ )
263+ . await
264+ . unwrap ( ) ;
265+ // update package.json
266+ fs. write (
267+ "/node_modules/lib/package.json" . into ( ) ,
268+ r#"{"version": "0.0.2"}"# . as_bytes ( ) ,
269+ )
270+ . await
271+ . unwrap ( ) ;
203272 let hash3 = helper. dir_hash ( & ArcPath :: from ( "/" ) ) . await . unwrap ( ) ;
204- assert_eq ! ( hash1 . hash, hash3. hash) ;
273+ assert_eq ! ( hash2 . hash, hash3. hash) ;
205274 assert_eq ! ( hash3. mtime, 0 ) ;
206275
207- helper . file_cache . clear ( ) ;
208- helper. dir_cache . clear ( ) ;
276+ // update file content
277+ let helper = new_helper ( fs . clone ( ) ) ;
209278 fs. write ( "/a/a2.js" . into ( ) , "a2a" . as_bytes ( ) ) . await . unwrap ( ) ;
210279 let hash4 = helper. dir_hash ( & ArcPath :: from ( "/" ) ) . await . unwrap ( ) ;
211- assert_ne ! ( hash1 . hash, hash4. hash) ;
280+ assert_ne ! ( hash3 . hash, hash4. hash) ;
212281 assert_eq ! ( hash4. mtime, 0 ) ;
282+
283+ // node_modules lib test
284+ let helper = new_helper ( fs. clone ( ) ) ;
285+ let hash1 = helper
286+ . dir_hash ( & ArcPath :: from ( "/node_modules/lib/" ) )
287+ . await
288+ . unwrap ( ) ;
289+
290+ // update lib content
291+ let helper = new_helper ( fs. clone ( ) ) ;
292+ fs. write (
293+ "/node_modules/lib/index.js" . into ( ) ,
294+ "const a = 3" . as_bytes ( ) ,
295+ )
296+ . await
297+ . unwrap ( ) ;
298+ let hash2 = helper
299+ . dir_hash ( & ArcPath :: from ( "/node_modules/lib/" ) )
300+ . await
301+ . unwrap ( ) ;
302+ assert_eq ! ( hash1. hash, hash2. hash) ;
303+
304+ // update package.json
305+ let helper = new_helper ( fs. clone ( ) ) ;
306+ fs. write (
307+ "/node_modules/lib/package.json" . into ( ) ,
308+ r#"{"version": "0.0.3"}"# . as_bytes ( ) ,
309+ )
310+ . await
311+ . unwrap ( ) ;
312+ let hash2 = helper
313+ . dir_hash ( & ArcPath :: from ( "/node_modules/lib/" ) )
314+ . await
315+ . unwrap ( ) ;
316+ assert_ne ! ( hash1. hash, hash2. hash) ;
213317 }
214318}
0 commit comments