@@ -16,6 +16,8 @@ struct ChallengePointer {
1616
1717#[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize ) ]
1818struct CacheData {
19+ /// For display/CLI purposes; filename becomes opaque once hashed.
20+ repo_key : String ,
1921 commit_hash : String ,
2022 challenge_pointers : Vec < ChallengePointer > ,
2123}
@@ -44,6 +46,12 @@ impl ChallengeCache {
4446 return Ok ( ( ) ) ;
4547 }
4648
49+ // Do not save when the commit hash is unknown
50+ let commit_str = match repo. commit_hash . as_deref ( ) {
51+ Some ( h) if !h. is_empty ( ) => h,
52+ _ => return Ok ( ( ) ) ,
53+ } ;
54+
4755 let cache_file = self . get_cache_file ( repo) ;
4856
4957 let challenge_pointers: Vec < ChallengePointer > = challenges
@@ -60,7 +68,8 @@ impl ChallengeCache {
6068 . collect ( ) ;
6169
6270 let cache_data = CacheData {
63- commit_hash : repo. commit_hash . clone ( ) . unwrap_or_default ( ) ,
71+ repo_key : repo. cache_key ( ) ,
72+ commit_hash : commit_str. to_string ( ) ,
6473 challenge_pointers,
6574 } ;
6675
@@ -93,10 +102,10 @@ impl ChallengeCache {
93102 let total = cache_data. challenge_pointers . len ( ) ;
94103 let processed = Arc :: new ( Mutex :: new ( 0usize ) ) ;
95104
96- let challenges : Vec < Challenge > = cache_data
105+ let results : Vec < Option < Challenge > > = cache_data
97106 . challenge_pointers
98107 . par_iter ( )
99- . filter_map ( |pointer| {
108+ . map ( |pointer| {
100109 let challenge = self . reconstruct_challenge ( pointer, repo_root) ;
101110
102111 // Report progress atomically
@@ -118,6 +127,10 @@ impl ChallengeCache {
118127 } )
119128 . collect ( ) ;
120129
130+ if results. iter ( ) . any ( |r| r. is_none ( ) ) {
131+ return None ;
132+ }
133+ let challenges: Vec < Challenge > = results. into_iter ( ) . map ( Option :: unwrap) . collect ( ) ;
121134 Some ( challenges)
122135 }
123136
@@ -172,22 +185,22 @@ impl ChallengeCache {
172185 return Ok ( Vec :: new ( ) ) ;
173186 }
174187
175- let keys = fs:: read_dir ( & self . cache_dir )
188+ let mut keys: Vec < String > = fs:: read_dir ( & self . cache_dir )
176189 . map_err ( |e| format ! ( "Failed to read cache dir: {}" , e) ) ?
177190 . filter_map ( |entry| entry. ok ( ) )
178191 . filter_map ( |entry| {
179- entry
180- . file_name ( )
181- . to_str ( )
182- . filter ( |name| name. ends_with ( ".bin" ) )
183- . and_then ( |name| {
184- let cache_key = name. trim_end_matches ( ".bin" ) ;
185- GzipStorage :: load :: < CacheData > ( & entry. path ( ) )
186- . map ( |cache_data| format ! ( "{}:{}" , cache_key, cache_data. commit_hash) )
187- } )
192+ // Prefer data inside the file (repo_key + commit_hash) instead of filename.
193+ if entry. file_name ( ) . to_string_lossy ( ) . ends_with ( ".bin" ) {
194+ GzipStorage :: load :: < CacheData > ( & entry. path ( ) )
195+ . map ( |d| format ! ( "{}:{}" , d. repo_key, d. commit_hash) )
196+ } else {
197+ None
198+ }
188199 } )
189200 . collect ( ) ;
190201
202+ keys. sort ( ) ;
203+ keys. dedup ( ) ;
191204 Ok ( keys)
192205 }
193206
@@ -198,6 +211,11 @@ impl ChallengeCache {
198211 ) -> Option < Challenge > {
199212 let file_path = pointer. source_file_path . as_ref ( ) ?;
200213 let absolute_path = repo_root. join ( file_path) ;
214+ let absolute_path = absolute_path. canonicalize ( ) . ok ( ) ?;
215+ let repo_root = repo_root. canonicalize ( ) . ok ( ) ?;
216+ if !absolute_path. starts_with ( & repo_root) {
217+ return None ;
218+ }
201219
202220 // Read file content
203221 let file_content = fs:: read_to_string ( & absolute_path) . ok ( ) ?;
@@ -228,9 +246,22 @@ impl ChallengeCache {
228246 }
229247
230248 fn get_cache_file ( & self , repo : & GitRepository ) -> PathBuf {
231- fs:: create_dir_all ( & self . cache_dir ) . ok ( ) ;
232- let cache_key = repo. cache_key ( ) ;
233- self . cache_dir . join ( format ! ( "{}.bin" , cache_key) )
249+ // Best-effort dir creation; callers handle save/load errors.
250+ let _ = fs:: create_dir_all ( & self . cache_dir ) ;
251+ // Compose a stable, collision-resistant, filesystem-safe key.
252+ let commit = repo. commit_hash . as_deref ( ) . unwrap_or ( "nohash" ) ;
253+ let dirty = if repo. is_dirty { "dirty" } else { "clean" } ;
254+ let raw = format ! ( "{}:{}:{}" , repo. cache_key( ) , commit, dirty) ;
255+ // Hash to keep filename short and safe across OSes.
256+ use sha2:: { Digest , Sha256 } ;
257+ let mut hasher = Sha256 :: new ( ) ;
258+ hasher. update ( raw. as_bytes ( ) ) ;
259+ let digest = hasher. finalize ( ) ;
260+ let hex = digest
261+ . iter ( )
262+ . map ( |b| format ! ( "{:02x}" , b) )
263+ . collect :: < String > ( ) ;
264+ self . cache_dir . join ( format ! ( "{}.bin" , hex) )
234265 }
235266}
236267
0 commit comments