11use crate :: utils:: { is_hex_chunk_path, truncate_middle, WadHashtable } ;
22use camino:: { Utf8Path , Utf8PathBuf } ;
33use color_eyre:: eyre:: { self , Ok } ;
4+ use dashmap:: DashMap ;
45use eyre:: Context ;
56use fancy_regex:: Regex ;
67use league_toolkit:: {
78 file:: LeagueFileKind ,
89 wad:: { decompress_raw, Wad , WadChunk } ,
910} ;
1011use std:: {
12+ collections:: HashMap ,
1113 fs:: { self , File , OpenOptions } ,
1214 io:: { self , Write } ,
1315 sync:: {
14- atomic:: { AtomicUsize , Ordering } ,
16+ atomic:: { AtomicU64 , AtomicUsize , Ordering } ,
1517 mpsc,
1618 } ,
1719} ;
@@ -20,8 +22,16 @@ use tracing_indicatif::style::ProgressStyle;
2022
2123const MAX_LOG_PATH_LEN : usize = 120 ;
2224
25+ pub struct ExtractStats {
26+ pub extracted_count : usize ,
27+ pub skipped_existing : usize ,
28+ pub error_count : usize ,
29+ pub bytes_written : u64 ,
30+ pub by_type : HashMap < LeagueFileKind , usize > ,
31+ }
32+
2333enum ChunkResult {
24- Extracted ,
34+ Extracted ( LeagueFileKind , u64 ) ,
2535 SkippedFilter ,
2636 SkippedExisting ,
2737}
@@ -62,7 +72,7 @@ impl<'a> Extractor<'a> {
6272 extract_directory : impl AsRef < Utf8Path > ,
6373 filter_type : Option < & [ LeagueFileKind ] > ,
6474 overwrite : bool ,
65- ) -> eyre:: Result < ( usize , usize ) > {
75+ ) -> eyre:: Result < ExtractStats > {
6676 let extract_directory = extract_directory. as_ref ( ) . to_path_buf ( ) ;
6777
6878 let chunks: Vec < WadChunk > = self . wad . chunks ( ) . iter ( ) . copied ( ) . collect ( ) ;
@@ -85,6 +95,9 @@ impl<'a> Extractor<'a> {
8595 let counter = AtomicUsize :: new ( 0 ) ;
8696 let extracted_counter = AtomicUsize :: new ( 0 ) ;
8797 let skipped_existing_counter = AtomicUsize :: new ( 0 ) ;
98+ let error_counter = AtomicUsize :: new ( 0 ) ;
99+ let bytes_written_counter = AtomicU64 :: new ( 0 ) ;
100+ let by_type: DashMap < LeagueFileKind , AtomicUsize > = DashMap :: new ( ) ;
88101 let filter_invert = self . filter_invert ;
89102 let extract_dir = & extract_directory;
90103 let err_holder: std:: sync:: Mutex < Option < eyre:: Report > > = std:: sync:: Mutex :: new ( None ) ;
@@ -97,6 +110,9 @@ impl<'a> Extractor<'a> {
97110 let counter = & counter;
98111 let extracted_counter = & extracted_counter;
99112 let skipped_existing_counter = & skipped_existing_counter;
113+ let error_counter = & error_counter;
114+ let bytes_written_counter = & bytes_written_counter;
115+ let by_type = & by_type;
100116 let err_holder = & err_holder;
101117 let progress_span = & span;
102118
@@ -112,14 +128,20 @@ impl<'a> Extractor<'a> {
112128 ) ;
113129
114130 match result {
115- std:: result:: Result :: Ok ( ChunkResult :: Extracted ) => {
131+ std:: result:: Result :: Ok ( ChunkResult :: Extracted ( kind , size ) ) => {
116132 extracted_counter. fetch_add ( 1 , Ordering :: Relaxed ) ;
133+ bytes_written_counter. fetch_add ( size, Ordering :: Relaxed ) ;
134+ by_type
135+ . entry ( kind)
136+ . or_insert_with ( || AtomicUsize :: new ( 0 ) )
137+ . fetch_add ( 1 , Ordering :: Relaxed ) ;
117138 }
118139 std:: result:: Result :: Ok ( ChunkResult :: SkippedExisting ) => {
119140 skipped_existing_counter. fetch_add ( 1 , Ordering :: Relaxed ) ;
120141 }
121142 std:: result:: Result :: Ok ( ChunkResult :: SkippedFilter ) => { }
122143 Err ( e) => {
144+ error_counter. fetch_add ( 1 , Ordering :: Relaxed ) ;
123145 let mut guard = err_holder. lock ( ) . unwrap ( ) ;
124146 if guard. is_none ( ) {
125147 * guard = Some ( e) ;
@@ -185,10 +207,18 @@ impl<'a> Extractor<'a> {
185207 return Err ( err) ;
186208 }
187209
188- Ok ( (
189- extracted_counter. load ( Ordering :: Relaxed ) ,
190- skipped_existing_counter. load ( Ordering :: Relaxed ) ,
191- ) )
210+ let by_type_map: HashMap < LeagueFileKind , usize > = by_type
211+ . into_iter ( )
212+ . map ( |( k, v) | ( k, v. load ( Ordering :: Relaxed ) ) )
213+ . collect ( ) ;
214+
215+ Ok ( ExtractStats {
216+ extracted_count : extracted_counter. load ( Ordering :: Relaxed ) ,
217+ skipped_existing : skipped_existing_counter. load ( Ordering :: Relaxed ) ,
218+ error_count : error_counter. load ( Ordering :: Relaxed ) ,
219+ bytes_written : bytes_written_counter. load ( Ordering :: Relaxed ) ,
220+ by_type : by_type_map,
221+ } )
192222 }
193223}
194224
@@ -220,8 +250,14 @@ fn process_chunk(
220250 fs:: create_dir_all ( parent. as_std_path ( ) ) ?;
221251 }
222252
253+ let size = chunk_data. len ( ) as u64 ;
223254 match write_chunk_file ( full_path. as_std_path ( ) , & chunk_data, overwrite) {
224- std:: result:: Result :: Ok ( result) => return Ok ( result) ,
255+ std:: result:: Result :: Ok ( ChunkWriteResult :: Written ) => {
256+ return Ok ( ChunkResult :: Extracted ( chunk_kind, size) ) ;
257+ }
258+ std:: result:: Result :: Ok ( ChunkWriteResult :: SkippedExisting ) => {
259+ return Ok ( ChunkResult :: SkippedExisting ) ;
260+ }
225261 Err ( error) if error. kind ( ) == io:: ErrorKind :: InvalidFilename => {
226262 return write_long_filename_chunk (
227263 chunk,
@@ -241,27 +277,32 @@ fn process_chunk(
241277 }
242278}
243279
280+ enum ChunkWriteResult {
281+ Written ,
282+ SkippedExisting ,
283+ }
284+
244285/// Writes chunk data to a file. When `overwrite` is false, uses `create_new(true)` for an
245286/// atomic existence check, returning `SkippedExisting` on `AlreadyExists`. This avoids the
246287/// TOCTOU race of a separate exists() check followed by write().
247288fn write_chunk_file (
248289 path : & std:: path:: Path ,
249290 data : & [ u8 ] ,
250291 overwrite : bool ,
251- ) -> io:: Result < ChunkResult > {
292+ ) -> io:: Result < ChunkWriteResult > {
252293 if overwrite {
253294 fs:: write ( path, data) ?;
254- return std:: result:: Result :: Ok ( ChunkResult :: Extracted ) ;
295+ return std:: result:: Result :: Ok ( ChunkWriteResult :: Written ) ;
255296 }
256297
257298 match OpenOptions :: new ( ) . write ( true ) . create_new ( true ) . open ( path) {
258299 std:: result:: Result :: Ok ( mut file) => {
259300 file. write_all ( data) ?;
260- std:: result:: Result :: Ok ( ChunkResult :: Extracted )
301+ std:: result:: Result :: Ok ( ChunkWriteResult :: Written )
261302 }
262303 Err ( e) if e. kind ( ) == io:: ErrorKind :: AlreadyExists => {
263304 tracing:: debug!( "skipping existing file: {}" , path. display( ) ) ;
264- std:: result:: Result :: Ok ( ChunkResult :: SkippedExisting )
305+ std:: result:: Result :: Ok ( ChunkWriteResult :: SkippedExisting )
265306 }
266307 Err ( e) => Err ( e) ,
267308 }
@@ -358,11 +399,11 @@ fn write_long_filename_chunk(
358399 & hashed_path
359400 ) ;
360401
361- Ok ( write_chunk_file (
362- full_path. as_std_path ( ) ,
363- chunk_data ,
364- overwrite ,
365- ) ? )
402+ let size = chunk_data . len ( ) as u64 ;
403+ match write_chunk_file ( full_path. as_std_path ( ) , chunk_data , overwrite ) ? {
404+ ChunkWriteResult :: Written => Ok ( ChunkResult :: Extracted ( chunk_kind , size ) ) ,
405+ ChunkWriteResult :: SkippedExisting => Ok ( ChunkResult :: SkippedExisting ) ,
406+ }
366407}
367408
368409#[ cfg( test) ]
0 commit comments