Skip to main content

wow_mpq/test_utils/
mpq_builder.rs

1//! MPQ test archive creation utilities
2//!
3//! Replaces the functionality of mpq_tools.py
4
5use crate::{ArchiveBuilder, FormatVersion, compression};
6use rand::{Rng, SeedableRng, rngs::StdRng};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Configuration for creating test MPQ archives
11#[derive(Debug, Clone)]
12pub struct TestArchiveConfig {
13    /// Name of the archive (used for filename)
14    pub name: String,
15    /// MPQ format version to use
16    pub version: FormatVersion,
17    /// List of files to include in the archive
18    pub files: Vec<TestFile>,
19    /// Hash table size (if None, automatically determined)
20    pub hash_table_size: Option<u32>,
21    /// Block size shift value (sector size = 512 << block_size)
22    pub block_size: Option<u8>,
23    /// Whether to manually include a (listfile) entry
24    pub include_listfile: bool,
25    /// Whether to include an (attributes) file
26    pub include_attributes: bool,
27}
28
29/// A file to include in the test archive
30#[derive(Debug, Clone)]
31pub struct TestFile {
32    /// Path/name of the file within the archive
33    pub name: String,
34    /// File content data
35    pub data: Vec<u8>,
36    /// Compression method flags (None for no compression)
37    pub compression: Option<u8>,
38    /// Whether the file should be encrypted
39    pub encrypted: bool,
40    /// Whether to use FIX_KEY encryption mode
41    pub fix_key: bool,
42}
43
44/// Type of test archive to create
45#[derive(Debug, Clone, Copy)]
46pub enum TestArchiveType {
47    /// Minimal archive with single file
48    Minimal,
49    /// Archive with compressed files
50    Compressed,
51    /// Archive with encrypted files
52    Encrypted,
53    /// Archive with various edge cases
54    EdgeCases,
55    /// Comprehensive test archive
56    Comprehensive,
57    /// Archive with CRC verification
58    WithCrc,
59}
60
61impl TestArchiveConfig {
62    /// Create a minimal test archive configuration
63    pub fn minimal(version: FormatVersion) -> Self {
64        Self {
65            name: format!("minimal_v{}", version as u8 + 1),
66            version,
67            files: vec![TestFile {
68                name: "test.txt".to_string(),
69                data: b"Hello, MPQ!".to_vec(),
70                compression: None,
71                encrypted: false,
72                fix_key: false,
73            }],
74            hash_table_size: Some(16),
75            block_size: Some(3),
76            include_listfile: version == FormatVersion::V1,
77            include_attributes: false,
78        }
79    }
80
81    /// Create a compressed files test archive
82    pub fn compressed(compression_type: &str) -> Self {
83        let data = generate_compressible_data(50 * 1024); // 50KB
84
85        let compression_flag = match compression_type {
86            "zlib" => Some(compression::flags::ZLIB),
87            "bzip2" => Some(compression::flags::BZIP2),
88            "lzma" => Some(compression::flags::LZMA),
89            "sparse" => Some(compression::flags::SPARSE),
90            _ => None,
91        };
92
93        Self {
94            name: format!("compressed_{compression_type}"),
95            version: FormatVersion::V2,
96            files: vec![
97                TestFile {
98                    name: "compressed.dat".to_string(),
99                    data: data.clone(),
100                    compression: compression_flag,
101                    encrypted: false,
102                    fix_key: false,
103                },
104                TestFile {
105                    name: "uncompressed.dat".to_string(),
106                    data: data[..1024].to_vec(),
107                    compression: None,
108                    encrypted: false,
109                    fix_key: false,
110                },
111            ],
112            hash_table_size: Some(32),
113            block_size: Some(4),
114            include_listfile: false,
115            include_attributes: false,
116        }
117    }
118
119    /// Create an encrypted files test archive
120    pub fn encrypted() -> Self {
121        Self {
122            name: "encrypted".to_string(),
123            version: FormatVersion::V2,
124            files: vec![
125                TestFile {
126                    name: "secret.dat".to_string(),
127                    data: b"This is encrypted data!".to_vec(),
128                    compression: Some(compression::flags::ZLIB),
129                    encrypted: true,
130                    fix_key: false,
131                },
132                TestFile {
133                    name: "fixed_key.dat".to_string(),
134                    data: b"This uses fix key encryption!".to_vec(),
135                    compression: None,
136                    encrypted: true,
137                    fix_key: true,
138                },
139            ],
140            hash_table_size: Some(16),
141            block_size: Some(3),
142            include_listfile: false,
143            include_attributes: false,
144        }
145    }
146
147    /// Create an edge cases test archive
148    pub fn edge_cases() -> Self {
149        Self {
150            name: "edge_cases".to_string(),
151            version: FormatVersion::V2,
152            files: vec![
153                // Empty file
154                TestFile {
155                    name: "empty.txt".to_string(),
156                    data: vec![],
157                    compression: None,
158                    encrypted: false,
159                    fix_key: false,
160                },
161                // Single byte file
162                TestFile {
163                    name: "single_byte.dat".to_string(),
164                    data: vec![0x42],
165                    compression: Some(compression::flags::ZLIB),
166                    encrypted: false,
167                    fix_key: false,
168                },
169                // File with spaces in name
170                TestFile {
171                    name: "file with spaces.txt".to_string(),
172                    data: b"Spaces in filename!".to_vec(),
173                    compression: None,
174                    encrypted: false,
175                    fix_key: false,
176                },
177                // File with path
178                TestFile {
179                    name: "folder/subfolder/nested.dat".to_string(),
180                    data: b"Nested file".to_vec(),
181                    compression: None,
182                    encrypted: false,
183                    fix_key: false,
184                },
185                // Large uncompressible file
186                TestFile {
187                    name: "random.bin".to_string(),
188                    data: generate_random_data(100 * 1024), // 100KB
189                    compression: Some(compression::flags::ZLIB),
190                    encrypted: false,
191                    fix_key: false,
192                },
193            ],
194            hash_table_size: Some(64),
195            block_size: Some(5),
196            include_listfile: true,
197            include_attributes: false,
198        }
199    }
200
201    /// Create a comprehensive test archive
202    pub fn comprehensive(version: FormatVersion) -> Self {
203        let mut files = vec![
204            TestFile {
205                name: "readme.txt".to_string(),
206                data: b"MPQ Archive Test Suite\n\nThis archive contains various test files."
207                    .to_vec(),
208                compression: None,
209                encrypted: false,
210                fix_key: false,
211            },
212            TestFile {
213                name: "data/config.ini".to_string(),
214                data: b"[Settings]\nversion=1.0\ntest=true".to_vec(),
215                compression: Some(compression::flags::ZLIB),
216                encrypted: false,
217                fix_key: false,
218            },
219            TestFile {
220                name: "data/binary.dat".to_string(),
221                data: generate_binary_pattern(10 * 1024),
222                compression: Some(compression::flags::BZIP2),
223                encrypted: false,
224                fix_key: false,
225            },
226            TestFile {
227                name: "secure/encrypted.bin".to_string(),
228                data: b"Secret data".to_vec(),
229                compression: None,
230                encrypted: true,
231                fix_key: false,
232            },
233        ];
234
235        // Add version-specific features
236        if version >= FormatVersion::V2 {
237            files.push(TestFile {
238                name: "large/bigfile.dat".to_string(),
239                data: generate_compressible_data(1024 * 1024), // 1MB
240                compression: Some(compression::flags::LZMA),
241                encrypted: false,
242                fix_key: false,
243            });
244        }
245
246        Self {
247            name: format!("comprehensive_v{}", version as u8 + 1),
248            version,
249            files,
250            hash_table_size: Some(128),
251            block_size: Some(7), // 64KB sectors
252            include_listfile: true,
253            include_attributes: version >= FormatVersion::V2,
254        }
255    }
256
257    /// Create test archive with CRC verification
258    pub fn with_crc() -> Self {
259        Self {
260            name: "crc_test".to_string(),
261            version: FormatVersion::V2,
262            files: vec![TestFile {
263                name: "crc_protected.dat".to_string(),
264                data: b"This file has CRC protection".to_vec(),
265                compression: Some(compression::flags::ZLIB),
266                encrypted: false,
267                fix_key: false,
268            }],
269            hash_table_size: Some(16),
270            block_size: Some(3),
271            include_listfile: false,
272            include_attributes: false,
273        }
274    }
275}
276
277/// Create a test MPQ archive
278pub fn create_test_archive(
279    output_path: &Path,
280    config: &TestArchiveConfig,
281) -> Result<PathBuf, crate::Error> {
282    let mut builder = if config.include_listfile {
283        // If we're manually including a listfile, don't auto-generate
284        ArchiveBuilder::new().listfile_option(crate::ListfileOption::None)
285    } else {
286        // Otherwise, auto-generate
287        ArchiveBuilder::new().listfile_option(crate::ListfileOption::Generate)
288    };
289
290    // Set version
291    builder = builder.version(config.version);
292
293    // Set block size if specified
294    if let Some(block_size) = config.block_size {
295        builder = builder.block_size(block_size.into());
296    }
297
298    // Note: hash_table_size is automatically determined by the builder
299
300    // Add files
301    for file in &config.files {
302        if file.encrypted {
303            builder = builder.add_file_data_with_encryption(
304                file.data.clone(),
305                &file.name,
306                file.compression.unwrap_or(0),
307                file.fix_key,
308                0, // locale
309            );
310        } else if let Some(compression) = file.compression {
311            builder = builder.add_file_data_with_options(
312                file.data.clone(),
313                &file.name,
314                compression,
315                false, // encrypt
316                0,     // locale
317            );
318        } else {
319            builder = builder.add_file_data(file.data.clone(), &file.name);
320        }
321    }
322
323    // Add (listfile) if requested
324    if config.include_listfile {
325        let listfile_content = config
326            .files
327            .iter()
328            .map(|f| f.name.as_str())
329            .collect::<Vec<_>>()
330            .join("\n");
331        builder = builder.add_file_data(listfile_content.into_bytes(), "(listfile)");
332    }
333
334    // Add (attributes) if requested
335    if config.include_attributes {
336        let attributes = generate_attributes(&config.files);
337        builder = builder.add_file_data(attributes, "(attributes)");
338    }
339
340    // Build the archive
341    let archive_path = output_path.join(&config.name).with_extension("mpq");
342    builder.build(&archive_path)?;
343
344    Ok(archive_path)
345}
346
347/// Generate compressible test data
348fn generate_compressible_data(size: usize) -> Vec<u8> {
349    let pattern = b"This is test data that should compress well because it has repeated patterns. ";
350    let mut data = Vec::with_capacity(size);
351
352    while data.len() < size {
353        let remaining = size - data.len();
354        let to_copy = remaining.min(pattern.len());
355        data.extend_from_slice(&pattern[..to_copy]);
356    }
357
358    data
359}
360
361/// Generate random uncompressible data
362fn generate_random_data(size: usize) -> Vec<u8> {
363    let mut rng = StdRng::seed_from_u64(42);
364    let mut data = vec![0u8; size];
365    rng.fill(&mut data[..]);
366    data
367}
368
369/// Generate binary pattern data
370fn generate_binary_pattern(size: usize) -> Vec<u8> {
371    let mut data = Vec::with_capacity(size);
372    let mut value = 0u8;
373
374    while data.len() < size {
375        data.push(value);
376        value = value.wrapping_add(1);
377    }
378
379    data
380}
381
382/// Generate attributes file content
383fn generate_attributes(files: &[TestFile]) -> Vec<u8> {
384    // Simple attributes format: CRC32 and timestamps
385    let mut attributes = Vec::new();
386
387    // Version
388    attributes.extend_from_slice(&100u32.to_le_bytes());
389
390    // Flags (CRC32 + TIMESTAMP)
391    attributes.extend_from_slice(&0x03u32.to_le_bytes());
392
393    // For each file: CRC32 and timestamp
394    for file in files {
395        // CRC32 (simplified - just use data length as fake CRC)
396        attributes.extend_from_slice(&(file.data.len() as u32).to_le_bytes());
397        // Timestamp (fake)
398        attributes.extend_from_slice(&0x5F000000u32.to_le_bytes());
399    }
400
401    attributes
402}
403
404/// Create all test archive types
405pub fn create_all_test_archives(output_dir: &Path) -> Result<Vec<PathBuf>, crate::Error> {
406    fs::create_dir_all(output_dir)?;
407    let mut created = Vec::new();
408
409    // Create minimal archives for each version
410    for version in [
411        FormatVersion::V1,
412        FormatVersion::V2,
413        FormatVersion::V3,
414        FormatVersion::V4,
415    ] {
416        let config = TestArchiveConfig::minimal(version);
417        let path = create_test_archive(output_dir, &config)?;
418        created.push(path);
419    }
420
421    // Create compressed archives
422    for compression in ["zlib", "bzip2", "lzma", "sparse"] {
423        let config = TestArchiveConfig::compressed(compression);
424        let path = create_test_archive(output_dir, &config)?;
425        created.push(path);
426    }
427
428    // Create other test types
429    let configs = vec![
430        TestArchiveConfig::encrypted(),
431        TestArchiveConfig::edge_cases(),
432        TestArchiveConfig::comprehensive(FormatVersion::V2),
433        TestArchiveConfig::comprehensive(FormatVersion::V4),
434        TestArchiveConfig::with_crc(),
435    ];
436
437    for config in configs {
438        let path = create_test_archive(output_dir, &config)?;
439        created.push(path);
440    }
441
442    Ok(created)
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use tempfile::TempDir;
449
450    #[test]
451    fn test_minimal_archive_creation() {
452        let temp_dir = TempDir::new().unwrap();
453        let config = TestArchiveConfig::minimal(FormatVersion::V1);
454        let result = create_test_archive(temp_dir.path(), &config).unwrap();
455
456        assert!(result.exists());
457        assert!(
458            result
459                .file_name()
460                .unwrap()
461                .to_str()
462                .unwrap()
463                .contains("minimal")
464        );
465    }
466
467    #[test]
468    fn test_compressed_archive_creation() {
469        let temp_dir = TempDir::new().unwrap();
470        let config = TestArchiveConfig::compressed("zlib");
471        let result = create_test_archive(temp_dir.path(), &config).unwrap();
472
473        assert!(result.exists());
474    }
475}