wow_mpq/
single_archive_parallel.rs1use crate::{Archive, Error, Result};
8use rayon::prelude::*;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12#[derive(Debug)]
37pub struct ParallelArchive {
38 path: PathBuf,
40 file_list: Arc<Vec<String>>,
42}
43
44impl ParallelArchive {
45 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
49 let path = path.as_ref().to_path_buf();
50
51 let mut archive = Archive::open(&path)?;
53 let entries = archive.list()?;
54 let file_list = Arc::new(entries.into_iter().map(|e| e.name).collect());
55
56 Ok(Self { path, file_list })
57 }
58
59 pub fn extract_files_parallel(&self, filenames: &[&str]) -> Result<Vec<(String, Vec<u8>)>> {
67 filenames
68 .par_iter()
69 .map(|&filename| {
70 let data = self.read_file_with_new_handle(filename)?;
72 Ok((filename.to_string(), data))
73 })
74 .collect()
75 }
76
77 pub fn extract_matching_parallel<F>(&self, predicate: F) -> Result<Vec<(String, Vec<u8>)>>
82 where
83 F: Fn(&str) -> bool + Sync,
84 {
85 let files = self.list_files();
87
88 files
90 .par_iter()
91 .filter(|name| predicate(name))
92 .map(|filename| {
93 let data = self.read_file_with_new_handle(filename)?;
94 Ok((filename.clone(), data))
95 })
96 .collect()
97 }
98
99 pub fn process_files_parallel<F, T>(&self, filenames: &[&str], processor: F) -> Result<Vec<T>>
103 where
104 F: Fn(&str, Vec<u8>) -> Result<T> + Sync,
105 T: Send,
106 {
107 filenames
108 .par_iter()
109 .map(|&filename| {
110 let data = self.read_file_with_new_handle(filename)?;
111 processor(filename, data)
112 })
113 .collect()
114 }
115
116 pub fn read_file_with_new_handle(&self, filename: &str) -> Result<Vec<u8>> {
121 let mut archive = Archive::open(&self.path)?;
123
124 archive.read_file(filename)
126 }
127
128 pub fn list_files(&self) -> &[String] {
130 &self.file_list
131 }
132
133 pub fn thread_count(&self) -> usize {
135 rayon::current_num_threads()
136 }
137
138 pub fn extract_files_batched(
143 &self,
144 filenames: &[&str],
145 batch_size: usize,
146 ) -> Result<Vec<(String, Vec<u8>)>> {
147 let chunks: Vec<_> = filenames.chunks(batch_size).collect();
149
150 let results: Result<Vec<_>> = chunks
152 .par_iter()
153 .map(|chunk| {
154 let mut archive = Archive::open(&self.path)?;
156
157 let mut batch_results = Vec::new();
159 for &filename in chunk.iter() {
160 let data = archive.read_file(filename)?;
161 batch_results.push((filename.to_string(), data));
162 }
163 Ok(batch_results)
164 })
165 .collect();
166
167 Ok(results?.into_iter().flatten().collect())
169 }
170}
171
172#[derive(Debug, Clone)]
174pub struct ParallelConfig {
175 pub num_threads: Option<usize>,
177 pub batch_size: usize,
179 pub skip_errors: bool,
181}
182
183impl Default for ParallelConfig {
184 fn default() -> Self {
185 Self {
186 num_threads: None,
187 batch_size: 10,
188 skip_errors: false,
189 }
190 }
191}
192
193impl ParallelConfig {
194 pub fn new() -> Self {
196 Self::default()
197 }
198
199 pub fn threads(mut self, num: usize) -> Self {
201 self.num_threads = Some(num);
202 self
203 }
204
205 pub fn batch_size(mut self, size: usize) -> Self {
207 self.batch_size = size;
208 self
209 }
210
211 pub fn skip_errors(mut self, skip: bool) -> Self {
213 self.skip_errors = skip;
214 self
215 }
216}
217
218pub fn extract_with_config<P: AsRef<Path>>(
223 archive_path: P,
224 filenames: &[&str],
225 config: ParallelConfig,
226) -> Result<Vec<(String, Result<Vec<u8>>)>> {
227 let use_batched = filenames.len() > 1000;
229
230 if use_batched {
231 extract_with_config_batched(archive_path, filenames, config)
232 } else {
233 extract_with_config_unbatched(archive_path, filenames, config)
234 }
235}
236
237fn extract_with_config_batched<P: AsRef<Path>>(
239 archive_path: P,
240 filenames: &[&str],
241 config: ParallelConfig,
242) -> Result<Vec<(String, Result<Vec<u8>>)>> {
243 let archive = ParallelArchive::open(archive_path)?;
244
245 let num_threads = config
247 .num_threads
248 .unwrap_or_else(rayon::current_num_threads);
249 let effective_batch_size = if filenames.len() > 5000 {
250 std::cmp::max(config.batch_size, filenames.len() / (num_threads * 2))
252 } else {
253 config.batch_size
254 };
255
256 let pool = if let Some(threads) = config.num_threads {
258 rayon::ThreadPoolBuilder::new()
259 .num_threads(threads)
260 .build()
261 .map_err(|e| {
262 Error::Io(std::io::Error::other(format!(
263 "Failed to create thread pool: {e}"
264 )))
265 })?
266 } else {
267 rayon::ThreadPoolBuilder::new().build().unwrap()
268 };
269
270 pool.install(|| {
272 let chunks: Vec<_> = filenames.chunks(effective_batch_size).collect();
274
275 let batch_results: Result<Vec<_>> = chunks
277 .par_iter()
278 .map(|chunk| {
279 let mut archive_handle = Archive::open(archive.path.as_path())?;
281
282 let mut batch_results = Vec::with_capacity(chunk.len());
284 for &filename in chunk.iter() {
285 let result = if config.skip_errors {
286 archive_handle.read_file(filename)
287 } else {
288 let data = archive_handle.read_file(filename)?;
289 Ok(data)
290 };
291 batch_results.push((filename.to_string(), result));
292 }
293 Ok(batch_results)
294 })
295 .collect();
296
297 match batch_results {
299 Ok(batches) => Ok(batches.into_iter().flatten().collect()),
300 Err(e) => Err(e),
301 }
302 })
303}
304
305fn extract_with_config_unbatched<P: AsRef<Path>>(
307 archive_path: P,
308 filenames: &[&str],
309 config: ParallelConfig,
310) -> Result<Vec<(String, Result<Vec<u8>>)>> {
311 let archive = ParallelArchive::open(archive_path)?;
312
313 let pool = if let Some(threads) = config.num_threads {
315 rayon::ThreadPoolBuilder::new()
316 .num_threads(threads)
317 .build()
318 .map_err(|e| {
319 Error::Io(std::io::Error::other(format!(
320 "Failed to create thread pool: {e}"
321 )))
322 })?
323 } else {
324 rayon::ThreadPoolBuilder::new().build().unwrap()
325 };
326
327 pool.install(|| {
329 if config.skip_errors {
330 Ok(filenames
332 .par_iter()
333 .map(|&filename| {
334 let result = archive.read_file_with_new_handle(filename);
335 (filename.to_string(), result)
336 })
337 .collect())
338 } else {
339 let results: Result<Vec<_>> = filenames
341 .par_iter()
342 .map(|&filename| {
343 let data = archive.read_file_with_new_handle(filename)?;
344 Ok((filename.to_string(), Ok(data)))
345 })
346 .collect();
347 results
348 }
349 })
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use crate::ArchiveBuilder;
356 use tempfile::TempDir;
357
358 fn create_test_archive() -> (TempDir, PathBuf) {
359 let temp = TempDir::new().unwrap();
360 let path = temp.path().join("test.mpq");
361
362 let mut builder = ArchiveBuilder::new();
363
364 for i in 0..20 {
366 let content = format!("File {i} content with some data to make it larger").repeat(100);
367 builder = builder.add_file_data(content.into_bytes(), &format!("file_{i:02}.txt"));
368 }
369
370 builder.build(&path).unwrap();
371 (temp, path)
372 }
373
374 #[test]
375 fn test_parallel_extraction() {
376 let (_temp, archive_path) = create_test_archive();
377 let archive = ParallelArchive::open(&archive_path).unwrap();
378
379 let files = vec!["file_00.txt", "file_05.txt", "file_10.txt", "file_15.txt"];
380 let results = archive.extract_files_parallel(&files).unwrap();
381
382 assert_eq!(results.len(), 4);
383 for (filename, data) in results {
384 assert!(!data.is_empty());
385 assert!(files.contains(&filename.as_str()));
386 }
387 }
388
389 #[test]
390 fn test_extract_matching() {
391 let (_temp, archive_path) = create_test_archive();
392 let archive = ParallelArchive::open(&archive_path).unwrap();
393
394 let results = archive
396 .extract_matching_parallel(|name| name.ends_with("5.txt"))
397 .unwrap();
398
399 assert_eq!(results.len(), 2); }
401
402 #[test]
403 fn test_batched_extraction() {
404 let (_temp, archive_path) = create_test_archive();
405 let archive = ParallelArchive::open(&archive_path).unwrap();
406
407 let files: Vec<&str> = (0..10)
408 .map(|i| Box::leak(format!("file_{i:02}.txt").into_boxed_str()) as &str)
409 .collect();
410
411 let results = archive.extract_files_batched(&files, 3).unwrap();
412 assert_eq!(results.len(), 10);
413 }
414
415 #[test]
416 fn test_custom_processing() {
417 let (_temp, archive_path) = create_test_archive();
418 let archive = ParallelArchive::open(&archive_path).unwrap();
419
420 let files = vec!["file_00.txt", "file_01.txt"];
421
422 let sizes = archive
424 .process_files_parallel(&files, |_name, data| Ok(data.len()))
425 .unwrap();
426
427 assert_eq!(sizes.len(), 2);
428 for size in sizes {
429 assert!(size > 0);
430 }
431 }
432
433 #[test]
434 fn test_with_config() {
435 let (_temp, archive_path) = create_test_archive();
436
437 let config = ParallelConfig::new()
438 .threads(2)
439 .batch_size(5)
440 .skip_errors(true);
441
442 let files = vec!["file_00.txt", "nonexistent.txt", "file_01.txt"];
443 let results = extract_with_config(&archive_path, &files, config).unwrap();
444
445 assert_eq!(results.len(), 3);
446 assert!(results[0].1.is_ok());
447 assert!(results[1].1.is_err());
448 assert!(results[2].1.is_ok());
449 }
450}