1use crate::{ArchiveBuilder, FormatVersion, compression};
6use rand::{Rng, SeedableRng, rngs::StdRng};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
12pub struct TestArchiveConfig {
13 pub name: String,
15 pub version: FormatVersion,
17 pub files: Vec<TestFile>,
19 pub hash_table_size: Option<u32>,
21 pub block_size: Option<u8>,
23 pub include_listfile: bool,
25 pub include_attributes: bool,
27}
28
29#[derive(Debug, Clone)]
31pub struct TestFile {
32 pub name: String,
34 pub data: Vec<u8>,
36 pub compression: Option<u8>,
38 pub encrypted: bool,
40 pub fix_key: bool,
42}
43
44#[derive(Debug, Clone, Copy)]
46pub enum TestArchiveType {
47 Minimal,
49 Compressed,
51 Encrypted,
53 EdgeCases,
55 Comprehensive,
57 WithCrc,
59}
60
61impl TestArchiveConfig {
62 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 pub fn compressed(compression_type: &str) -> Self {
83 let data = generate_compressible_data(50 * 1024); 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 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 pub fn edge_cases() -> Self {
149 Self {
150 name: "edge_cases".to_string(),
151 version: FormatVersion::V2,
152 files: vec![
153 TestFile {
155 name: "empty.txt".to_string(),
156 data: vec![],
157 compression: None,
158 encrypted: false,
159 fix_key: false,
160 },
161 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 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 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 TestFile {
187 name: "random.bin".to_string(),
188 data: generate_random_data(100 * 1024), 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 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 if version >= FormatVersion::V2 {
237 files.push(TestFile {
238 name: "large/bigfile.dat".to_string(),
239 data: generate_compressible_data(1024 * 1024), 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), include_listfile: true,
253 include_attributes: version >= FormatVersion::V2,
254 }
255 }
256
257 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
277pub 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 ArchiveBuilder::new().listfile_option(crate::ListfileOption::None)
285 } else {
286 ArchiveBuilder::new().listfile_option(crate::ListfileOption::Generate)
288 };
289
290 builder = builder.version(config.version);
292
293 if let Some(block_size) = config.block_size {
295 builder = builder.block_size(block_size.into());
296 }
297
298 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, );
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, 0, );
318 } else {
319 builder = builder.add_file_data(file.data.clone(), &file.name);
320 }
321 }
322
323 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 if config.include_attributes {
336 let attributes = generate_attributes(&config.files);
337 builder = builder.add_file_data(attributes, "(attributes)");
338 }
339
340 let archive_path = output_path.join(&config.name).with_extension("mpq");
342 builder.build(&archive_path)?;
343
344 Ok(archive_path)
345}
346
347fn 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
361fn 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
369fn 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
382fn generate_attributes(files: &[TestFile]) -> Vec<u8> {
384 let mut attributes = Vec::new();
386
387 attributes.extend_from_slice(&100u32.to_le_bytes());
389
390 attributes.extend_from_slice(&0x03u32.to_le_bytes());
392
393 for file in files {
395 attributes.extend_from_slice(&(file.data.len() as u32).to_le_bytes());
397 attributes.extend_from_slice(&0x5F000000u32.to_le_bytes());
399 }
400
401 attributes
402}
403
404pub 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 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 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 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}