Skip to main content

wow_mpq/
archive.rs

1//! MPQ archive handling
2//!
3//! This module provides the main Archive type for reading MPQ files.
4//! It supports:
5//! - All MPQ versions (v1-v4)
6//! - File extraction with decompression
7//! - Sector CRC validation
8//! - Encryption/decryption
9//! - Multi-sector and single-unit files
10
11use crate::{
12    Error, Result,
13    builder::ArchiveBuilder,
14    compression,
15    crypto::{decrypt_block, decrypt_dword, hash_string, hash_type},
16    header::{self, MpqHeader, UserDataHeader},
17    special_files,
18    tables::{BetTable, BlockTable, HashTable, HetTable, HiBlockTable},
19};
20use byteorder::{LittleEndian, ReadBytesExt};
21use std::fs::File;
22use std::io::{BufReader, Read, Seek, SeekFrom};
23use std::path::{Path, PathBuf};
24
25/// Detailed information about an MPQ archive
26#[derive(Debug, Clone)]
27pub struct ArchiveInfo {
28    /// Path to the archive file
29    pub path: PathBuf,
30    /// Total file size in bytes
31    pub file_size: u64,
32    /// Archive offset (if MPQ data starts after user data)
33    pub archive_offset: u64,
34    /// MPQ format version
35    pub format_version: header::FormatVersion,
36    /// Number of files in the archive
37    pub file_count: usize,
38    /// Maximum file capacity (hash table size)
39    pub max_file_count: u32,
40    /// Sector size in bytes
41    pub sector_size: usize,
42    /// Archive is encrypted
43    pub is_encrypted: bool,
44    /// Archive has digital signature
45    pub has_signature: bool,
46    /// Signature status (if applicable)
47    pub signature_status: SignatureStatus,
48    /// Hash table information
49    pub hash_table_info: TableInfo,
50    /// Block table information
51    pub block_table_info: TableInfo,
52    /// HET table information (v3+)
53    pub het_table_info: Option<TableInfo>,
54    /// BET table information (v3+)
55    pub bet_table_info: Option<TableInfo>,
56    /// Hi-block table information (v2+)
57    pub hi_block_table_info: Option<TableInfo>,
58    /// Has (attributes) file
59    pub has_attributes: bool,
60    /// Has (listfile) file
61    pub has_listfile: bool,
62    /// User data information
63    pub user_data_info: Option<UserDataInfo>,
64    /// MD5 checksums status (v4)
65    pub md5_status: Option<Md5Status>,
66}
67
68/// Information about a table in the archive
69#[derive(Debug, Clone)]
70pub struct TableInfo {
71    /// Table size in entries (None if table failed to load)
72    pub size: Option<u32>,
73    /// Table offset in archive
74    pub offset: u64,
75    /// Compressed size (if applicable)
76    pub compressed_size: Option<u64>,
77    /// Whether the table failed to load
78    pub failed_to_load: bool,
79}
80
81/// User data information
82#[derive(Debug, Clone)]
83pub struct UserDataInfo {
84    /// User data header size
85    pub header_size: u32,
86    /// User data size
87    pub data_size: u32,
88}
89
90/// Digital signature status
91#[derive(Debug, Clone, PartialEq)]
92pub enum SignatureStatus {
93    /// No signature present
94    None,
95    /// Weak signature present and valid
96    WeakValid,
97    /// Weak signature present but invalid
98    WeakInvalid,
99    /// Strong signature present and valid
100    StrongValid,
101    /// Strong signature present but invalid
102    StrongInvalid,
103    /// Strong signature present but no public key available
104    StrongNoKey,
105}
106
107/// MD5 checksum verification status for v4 archives
108#[derive(Debug, Clone)]
109pub struct Md5Status {
110    /// Hash table MD5 valid
111    pub hash_table_valid: bool,
112    /// Block table MD5 valid
113    pub block_table_valid: bool,
114    /// Hi-block table MD5 valid
115    pub hi_block_table_valid: bool,
116    /// HET table MD5 valid
117    pub het_table_valid: bool,
118    /// BET table MD5 valid
119    pub bet_table_valid: bool,
120    /// MPQ header MD5 valid
121    pub header_valid: bool,
122}
123
124/// Options for opening MPQ archives
125///
126/// This struct provides configuration options for how MPQ archives are opened
127/// and initialized. It follows the builder pattern for easy configuration.
128///
129/// # Examples
130///
131/// ```no_run
132/// use wow_mpq::{Archive, OpenOptions};
133///
134/// // Open with default options
135/// let archive = Archive::open("data.mpq")?;
136///
137/// // Open with custom options
138/// let archive = OpenOptions::new()
139///     .load_tables(false)  // Defer table loading for faster startup
140///     .open("data.mpq")?;
141/// # Ok::<(), wow_mpq::Error>(())
142/// ```
143#[derive(Debug, Clone)]
144pub struct OpenOptions {
145    /// Whether to load and parse all tables immediately when opening the archive.
146    ///
147    /// When `true` (default), all tables (hash, block, HET/BET) are loaded and
148    /// validated during archive opening. This provides immediate error detection
149    /// but slower startup for large archives.
150    ///
151    /// When `false`, tables are loaded on-demand when first accessed. This
152    /// provides faster startup but may defer error detection.
153    pub load_tables: bool,
154
155    /// MPQ format version to use when creating new archives.
156    ///
157    /// This field is only used when creating new archives via `create()`.
158    /// If `None`, defaults to MPQ version 1 for maximum compatibility.
159    version: Option<crate::header::FormatVersion>,
160}
161
162impl OpenOptions {
163    /// Create new default options
164    ///
165    /// Returns an `OpenOptions` instance with default settings:
166    /// - `load_tables = true` (immediate table loading)
167    /// - `version = None` (defaults to MPQ v1 for new archives)
168    pub fn new() -> Self {
169        Self {
170            load_tables: true,
171            version: None,
172        }
173    }
174
175    /// Set whether to load tables immediately when opening
176    ///
177    /// # Parameters
178    /// - `load`: If `true`, tables are loaded immediately during open.
179    ///   If `false`, tables are loaded on first access.
180    ///
181    /// # Returns
182    /// Self for method chaining
183    pub fn load_tables(mut self, load: bool) -> Self {
184        self.load_tables = load;
185        self
186    }
187
188    /// Set the MPQ version for new archives
189    ///
190    /// This setting only affects archives created with `create()`, not
191    /// archives opened with `open()`.
192    ///
193    /// # Parameters
194    /// - `version`: The MPQ format version to use (V1, V2, V3, or V4)
195    ///
196    /// # Returns
197    /// Self for method chaining
198    pub fn version(mut self, version: crate::header::FormatVersion) -> Self {
199        self.version = Some(version);
200        self
201    }
202
203    /// Open an existing MPQ archive with these options
204    ///
205    /// # Parameters
206    /// - `path`: Path to the MPQ archive file
207    ///
208    /// # Returns
209    /// `Ok(Archive)` on success, `Err(Error)` on failure
210    ///
211    /// # Errors
212    /// - `Error::Io` if the file cannot be opened
213    /// - `Error::InvalidFormat` if the file is not a valid MPQ archive
214    /// - `Error::Corruption` if table validation fails (when `load_tables = true`)
215    pub fn open<P: AsRef<Path>>(self, path: P) -> Result<Archive> {
216        Archive::open_with_options(path, self)
217    }
218
219    /// Create a new empty MPQ archive with these options
220    ///
221    /// Creates a new MPQ archive file with the specified format version.
222    /// The archive will be empty but properly formatted.
223    ///
224    /// # Parameters
225    /// - `path`: Path where the new archive should be created
226    ///
227    /// # Returns
228    /// `Ok(Archive)` on success, `Err(Error)` on failure
229    ///
230    /// # Errors
231    /// - `Error::Io` if the file cannot be created
232    /// - `Error::InvalidFormat` if archive creation fails
233    pub fn create<P: AsRef<Path>>(self, path: P) -> Result<Archive> {
234        let path = path.as_ref();
235
236        // Create an empty archive with the specified version
237        let builder =
238            ArchiveBuilder::new().version(self.version.unwrap_or(crate::header::FormatVersion::V1));
239
240        // Build the empty archive
241        builder.build(path)?;
242
243        // Open the newly created archive
244        Self::new().load_tables(self.load_tables).open(path)
245    }
246}
247
248impl Default for OpenOptions {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254/// An MPQ archive
255#[derive(Debug)]
256pub struct Archive {
257    /// Path to the archive file
258    path: PathBuf,
259    /// Archive file reader
260    reader: BufReader<File>,
261    /// Offset where the MPQ data starts in the file
262    archive_offset: u64,
263    /// Optional user data header
264    user_data: Option<UserDataHeader>,
265    /// MPQ header
266    header: MpqHeader,
267    /// Hash table (optional, loaded on demand)
268    hash_table: Option<HashTable>,
269    /// Block table (optional, loaded on demand)
270    block_table: Option<BlockTable>,
271    /// Hi-block table for v2+ archives (optional)
272    hi_block_table: Option<HiBlockTable>,
273    /// HET table for v3+ archives
274    het_table: Option<HetTable>,
275    /// BET table for v3+ archives
276    bet_table: Option<BetTable>,
277    /// File attributes from (attributes) file
278    attributes: Option<special_files::Attributes>,
279}
280
281impl Archive {
282    /// Open an existing MPQ archive
283    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
284        Self::open_with_options(path, OpenOptions::default())
285    }
286
287    /// Open an archive with specific options
288    pub fn open_with_options<P: AsRef<Path>>(path: P, options: OpenOptions) -> Result<Self> {
289        let path = path.as_ref().to_path_buf();
290        let file = File::open(&path)?;
291        let mut reader = BufReader::new(file);
292
293        // Find and read the MPQ header
294        let (archive_offset, user_data, header) = header::find_header(&mut reader)?;
295
296        let mut archive = Archive {
297            path,
298            reader,
299            archive_offset,
300            user_data,
301            header,
302            hash_table: None,
303            block_table: None,
304            hi_block_table: None,
305            bet_table: None,
306            het_table: None,
307            attributes: None,
308        };
309
310        // Load tables if requested
311        if options.load_tables {
312            archive.load_tables()?;
313        }
314
315        Ok(archive)
316    }
317
318    /// Load hash and block tables
319    pub fn load_tables(&mut self) -> Result<()> {
320        log::debug!(
321            "Loading tables for archive version {:?}",
322            self.header.format_version
323        );
324
325        // For v3+ archives, check for HET/BET tables first
326        if self.header.format_version >= header::FormatVersion::V3 {
327            // Try to load HET table
328            if let Some(het_pos) = self.header.het_table_pos
329                && het_pos != 0
330            {
331                let mut het_size = self
332                    .header
333                    .v4_data
334                    .as_ref()
335                    .map(|v4| v4.het_table_size_64)
336                    .unwrap_or(0);
337
338                // For V3 without V4 data, we need to determine the size
339                if het_size == 0 && self.header.format_version == header::FormatVersion::V3 {
340                    log::debug!("V3 archive without V4 data, reading HET table size from header");
341                    // Try to read the table size from the HET header
342                    match self.read_het_table_size(het_pos) {
343                        Ok(size) => {
344                            log::debug!("Determined HET table size: 0x{size:X}");
345                            het_size = size;
346                        }
347                        Err(e) => {
348                            log::warn!("Failed to determine HET table size: {e}");
349                        }
350                    }
351                }
352
353                if het_size > 0 {
354                    log::debug!("Loading HET table from offset 0x{het_pos:X}, size 0x{het_size:X}");
355
356                    // HET table key is based on table name
357                    let key = hash_string("(hash table)", hash_type::FILE_KEY);
358
359                    match HetTable::read(
360                        &mut self.reader,
361                        self.archive_offset + het_pos,
362                        het_size,
363                        key,
364                    ) {
365                        Ok(het) => {
366                            let file_count = het.header.max_file_count;
367                            log::info!("Loaded HET table with {file_count} max files");
368                            self.het_table = Some(het);
369                        }
370                        Err(e) => {
371                            log::warn!("Failed to load HET table: {e}");
372                        }
373                    }
374                }
375            }
376
377            // Try to load BET table
378            if let Some(bet_pos) = self.header.bet_table_pos
379                && bet_pos != 0
380            {
381                let mut bet_size = self
382                    .header
383                    .v4_data
384                    .as_ref()
385                    .map(|v4| v4.bet_table_size_64)
386                    .unwrap_or(0);
387
388                // For V3 without V4 data, we need to determine the size
389                if bet_size == 0 && self.header.format_version == header::FormatVersion::V3 {
390                    log::debug!("V3 archive without V4 data, reading BET table size from header");
391                    // Try to read the table size from the BET header
392                    match self.read_bet_table_size(bet_pos) {
393                        Ok(size) => {
394                            log::debug!("Determined BET table size: 0x{size:X}");
395                            bet_size = size;
396                        }
397                        Err(e) => {
398                            log::warn!("Failed to determine BET table size: {e}");
399                        }
400                    }
401                }
402
403                if bet_size > 0 {
404                    log::debug!("Loading BET table from offset 0x{bet_pos:X}, size 0x{bet_size:X}");
405
406                    // First, check if the BET offset actually points to a HET table
407                    // This is a known issue in some MoP update archives
408                    self.reader
409                        .seek(SeekFrom::Start(self.archive_offset + bet_pos))?;
410                    let mut sig_buf = [0u8; 4];
411                    self.reader.read_exact(&mut sig_buf)?;
412
413                    if &sig_buf == b"HET\x1A" {
414                        log::error!(
415                            "BET offset points to HET table! This archive has swapped table offsets."
416                        );
417                        log::warn!(
418                            "Skipping BET table loading for this archive due to invalid offset."
419                        );
420                    } else {
421                        // Reset position and proceed with normal BET loading
422                        self.reader
423                            .seek(SeekFrom::Start(self.archive_offset + bet_pos))?;
424
425                        // BET table key is based on table name
426                        let key = hash_string("(block table)", hash_type::FILE_KEY);
427
428                        match BetTable::read(
429                            &mut self.reader,
430                            self.archive_offset + bet_pos,
431                            bet_size,
432                            key,
433                        ) {
434                            Ok(bet) => {
435                                let file_count = bet.header.file_count;
436                                log::info!("Loaded BET table with {file_count} files");
437                                self.bet_table = Some(bet);
438                            }
439                            Err(e) => {
440                                log::warn!("Failed to load BET table: {e}");
441                            }
442                        }
443                    }
444                }
445            }
446        }
447
448        // Check if we have valid HET/BET tables with actual entries
449        let _has_valid_het_bet = match (&self.het_table, &self.bet_table) {
450            (Some(het), Some(bet)) => {
451                // Tables are valid if they have entries
452                het.header.max_file_count > 0 && bet.header.file_count > 0
453            }
454            _ => false,
455        };
456
457        // Always try to load classic tables if they exist (for compatibility)
458        // Only skip them if the archive appears to be truncated/corrupted
459        if self.header.hash_table_size > 0 {
460            // Load hash table
461            let hash_table_offset = self.archive_offset + self.header.get_hash_table_pos();
462            let uncompressed_size = self.header.hash_table_size as usize * 16; // Each hash entry is 16 bytes
463
464            // For V4 archives, we have explicit compressed size info
465            if let Some(v4_data) = &self.header.v4_data {
466                // Validate V4 sizes are reasonable (not corrupted)
467                let file_size = self.reader.get_ref().metadata()?.len();
468                let v4_size_valid = v4_data.hash_table_size_64 > 0
469                    && v4_data.hash_table_size_64 < file_size
470                    && v4_data.hash_table_size_64 < (uncompressed_size as u64 * 2); // Compressed shouldn't be much larger
471
472                if v4_size_valid {
473                    // Use compressed size for V4
474                    let compressed_size = v4_data.hash_table_size_64;
475
476                    log::debug!(
477                        "Loading hash table from 0x{hash_table_offset:X}, compressed size: {compressed_size} bytes, uncompressed size: {uncompressed_size} bytes"
478                    );
479
480                    // Check if it would extend beyond file
481                    let file_size = self.reader.get_ref().metadata()?.len();
482                    if hash_table_offset + compressed_size > file_size {
483                        log::warn!("Hash table extends beyond file, skipping");
484                    } else {
485                        // V4 tables are encrypted on disk; decrypt before decompressing,
486                        // then parse without re-decrypting.
487                        let key = hash_string("(hash table)", hash_type::FILE_KEY);
488                        match self.read_compressed_encrypted_table(
489                            hash_table_offset,
490                            compressed_size,
491                            uncompressed_size,
492                            key,
493                        ) {
494                            Ok(table_data) => {
495                                // Data is already decrypted — use from_bytes_decrypted
496                                match HashTable::from_bytes_decrypted(
497                                    &table_data,
498                                    self.header.hash_table_size,
499                                ) {
500                                    Ok(hash_table) => {
501                                        self.hash_table = Some(hash_table);
502                                    }
503                                    Err(e) => {
504                                        log::warn!("Failed to parse hash table: {e}");
505                                    }
506                                }
507                            }
508                            Err(e) => {
509                                log::warn!("Failed to decompress hash table: {e}");
510                            }
511                        }
512                    }
513                } else {
514                    // V4 sizes are invalid, fall back to V3-style detection
515                    log::warn!(
516                        "V4 archive has invalid compressed size ({}), using heuristic detection",
517                        v4_data.hash_table_size_64
518                    );
519                    // Fall through to V3-style detection below
520                }
521            }
522
523            // If we don't have valid V4 data or V4 size was invalid, use heuristic
524            if self.hash_table.is_none() {
525                // For V3 and earlier, or V4 with invalid sizes, we need to detect if tables are compressed
526                // by checking the available space between tables
527                let block_table_offset = self.archive_offset + self.header.get_block_table_pos();
528                let available_space = if block_table_offset > hash_table_offset {
529                    (block_table_offset - hash_table_offset) as usize
530                } else {
531                    // If block table comes before hash table, calculate differently
532                    let file_size = self.reader.get_ref().metadata()?.len();
533                    (file_size - hash_table_offset) as usize
534                };
535
536                if available_space < uncompressed_size {
537                    // Table appears to be compressed
538                    log::debug!(
539                        "V3 hash table appears compressed: available space {available_space} < expected size {uncompressed_size}"
540                    );
541
542                    // Try to read as compressed
543                    match self.read_compressed_table(
544                        hash_table_offset,
545                        available_space as u64,
546                        uncompressed_size,
547                    ) {
548                        Ok(table_data) => {
549                            match HashTable::from_bytes(&table_data, self.header.hash_table_size) {
550                                Ok(hash_table) => {
551                                    self.hash_table = Some(hash_table);
552                                }
553                                Err(e) => {
554                                    log::warn!("Failed to parse hash table: {e}");
555                                }
556                            }
557                        }
558                        Err(e) => {
559                            log::warn!("Failed to decompress hash table: {e}");
560                            // Try to read as truncated uncompressed table
561                            // Calculate how many entries we can fit in available space
562                            let entries_that_fit = available_space / 16; // 16 bytes per entry
563                            // Round down to nearest power of 2 for hash table
564                            let mut pow2_entries = 1u32;
565                            while pow2_entries * 2 <= entries_that_fit as u32 {
566                                pow2_entries *= 2;
567                            }
568                            if pow2_entries > 0 {
569                                log::warn!(
570                                    "Trying to read truncated hash table with {} entries (originally {})",
571                                    pow2_entries,
572                                    self.header.hash_table_size
573                                );
574                                match HashTable::read(
575                                    &mut self.reader,
576                                    hash_table_offset,
577                                    pow2_entries,
578                                ) {
579                                    Ok(hash_table) => {
580                                        self.hash_table = Some(hash_table);
581                                        log::info!("Successfully loaded truncated hash table");
582                                    }
583                                    Err(e2) => {
584                                        log::warn!("Failed to read truncated hash table: {e2}");
585                                    }
586                                }
587                            }
588                        }
589                    }
590                } else {
591                    // Normal uncompressed reading
592                    match HashTable::read(
593                        &mut self.reader,
594                        hash_table_offset,
595                        self.header.hash_table_size,
596                    ) {
597                        Ok(hash_table) => {
598                            self.hash_table = Some(hash_table);
599                        }
600                        Err(e) => {
601                            log::warn!("Failed to read hash table: {e}");
602                        }
603                    }
604                }
605            }
606        }
607
608        if self.header.block_table_size > 0 {
609            // Load block table
610            let block_table_offset = self.archive_offset + self.header.get_block_table_pos();
611            let uncompressed_size = self.header.block_table_size as usize * 16; // Each block entry is 16 bytes
612
613            // For V4 archives, we have explicit compressed size info
614            if let Some(v4_data) = &self.header.v4_data {
615                // Validate V4 sizes are reasonable (not corrupted)
616                let file_size = self.reader.get_ref().metadata()?.len();
617                let v4_size_valid = v4_data.block_table_size_64 > 0
618                    && v4_data.block_table_size_64 < file_size
619                    && v4_data.block_table_size_64 < (uncompressed_size as u64 * 2); // Compressed shouldn't be much larger
620
621                if v4_size_valid {
622                    // Use compressed size for V4
623                    let compressed_size = v4_data.block_table_size_64;
624
625                    log::debug!(
626                        "Loading block table from 0x{block_table_offset:X}, compressed size: {compressed_size} bytes, uncompressed size: {uncompressed_size} bytes"
627                    );
628
629                    // Check if it would extend beyond file
630                    let file_size = self.reader.get_ref().metadata()?.len();
631                    if block_table_offset + compressed_size > file_size {
632                        log::warn!("Block table extends beyond file, skipping");
633                    } else {
634                        // V4 tables are encrypted on disk; decrypt before decompressing,
635                        // then parse without re-decrypting.
636                        let key = hash_string("(block table)", hash_type::FILE_KEY);
637                        match self.read_compressed_encrypted_table(
638                            block_table_offset,
639                            compressed_size,
640                            uncompressed_size,
641                            key,
642                        ) {
643                            Ok(table_data) => {
644                                // Data is already decrypted — use from_bytes_decrypted
645                                match BlockTable::from_bytes_decrypted(
646                                    &table_data,
647                                    self.header.block_table_size,
648                                ) {
649                                    Ok(block_table) => {
650                                        self.block_table = Some(block_table);
651                                    }
652                                    Err(e) => {
653                                        log::warn!("Failed to parse block table: {e}");
654                                    }
655                                }
656                            }
657                            Err(e) => {
658                                log::warn!("Failed to decompress block table: {e}");
659                            }
660                        }
661                    }
662                } else {
663                    // V4 sizes are invalid, fall back to V3-style detection
664                    log::warn!(
665                        "V4 archive has invalid compressed size ({}), using heuristic detection",
666                        v4_data.block_table_size_64
667                    );
668                    // Fall through to V3-style detection below
669                }
670            }
671
672            // If we don't have valid V4 data or V4 size was invalid, use heuristic
673            if self.block_table.is_none() {
674                // For V3 and earlier, or V4 with invalid sizes, we need to detect if tables are compressed
675                // Calculate available space for block table
676                let file_size = self.reader.get_ref().metadata()?.len();
677                let next_section = if let Some(hi_block_pos) = self.header.hi_block_table_pos {
678                    if hi_block_pos != 0 {
679                        self.archive_offset + hi_block_pos
680                    } else {
681                        file_size
682                    }
683                } else {
684                    file_size
685                };
686
687                let available_space = (next_section.saturating_sub(block_table_offset)) as usize;
688
689                if available_space < uncompressed_size {
690                    // Table appears to be compressed
691                    log::debug!(
692                        "V3 block table appears compressed: available space {available_space} < expected size {uncompressed_size}"
693                    );
694
695                    // Try to read as compressed
696                    match self.read_compressed_table(
697                        block_table_offset,
698                        available_space as u64,
699                        uncompressed_size,
700                    ) {
701                        Ok(table_data) => {
702                            match BlockTable::from_bytes(&table_data, self.header.block_table_size)
703                            {
704                                Ok(block_table) => {
705                                    self.block_table = Some(block_table);
706                                }
707                                Err(e) => {
708                                    log::warn!("Failed to parse block table: {e}");
709                                }
710                            }
711                        }
712                        Err(e) => {
713                            log::warn!("Failed to decompress block table: {e}");
714                            // Try to read as truncated uncompressed table
715                            // Calculate how many entries we can fit in available space
716                            let entries_that_fit = available_space / 16; // 16 bytes per entry
717                            if entries_that_fit > 0 {
718                                log::warn!(
719                                    "Trying to read truncated block table with {} entries (originally {})",
720                                    entries_that_fit,
721                                    self.header.block_table_size
722                                );
723                                match BlockTable::read(
724                                    &mut self.reader,
725                                    block_table_offset,
726                                    entries_that_fit as u32,
727                                ) {
728                                    Ok(block_table) => {
729                                        self.block_table = Some(block_table);
730                                        log::info!("Successfully loaded truncated block table");
731                                    }
732                                    Err(e2) => {
733                                        log::warn!("Failed to read truncated block table: {e2}");
734                                    }
735                                }
736                            }
737                        }
738                    }
739                } else {
740                    // Normal uncompressed reading
741                    match BlockTable::read(
742                        &mut self.reader,
743                        block_table_offset,
744                        self.header.block_table_size,
745                    ) {
746                        Ok(block_table) => {
747                            self.block_table = Some(block_table);
748                        }
749                        Err(e) => {
750                            log::warn!("Failed to read block table: {e}");
751                        }
752                    }
753                }
754            }
755        }
756
757        // Load hi-block table if present (v2+)
758        if let Some(hi_block_pos) = self.header.hi_block_table_pos
759            && hi_block_pos != 0
760        {
761            let hi_block_offset = self.archive_offset + hi_block_pos;
762            let hi_block_end = hi_block_offset + (self.header.block_table_size as u64 * 8);
763
764            let file_size = self.reader.get_ref().metadata()?.len();
765            if hi_block_end > file_size {
766                log::warn!(
767                    "Hi-block table extends beyond file (ends at 0x{hi_block_end:X}, file size 0x{file_size:X}). Skipping."
768                );
769            } else {
770                self.hi_block_table = Some(HiBlockTable::read(
771                    &mut self.reader,
772                    hi_block_offset,
773                    self.header.block_table_size,
774                )?);
775            }
776        }
777
778        // Load attributes if present
779        match self.load_attributes() {
780            Ok(()) => {}
781            Err(e) => {
782                log::warn!("Failed to load attributes: {e:?}");
783                // Continue without attributes
784            }
785        }
786
787        Ok(())
788    }
789
790    /// Get the archive header
791    pub fn header(&self) -> &MpqHeader {
792        &self.header
793    }
794
795    /// Get the user data header if present
796    pub fn user_data(&self) -> Option<&UserDataHeader> {
797        self.user_data.as_ref()
798    }
799
800    /// Get the archive offset in the file
801    pub fn archive_offset(&self) -> u64 {
802        self.archive_offset
803    }
804
805    /// Get the path to the archive
806    pub fn path(&self) -> &Path {
807        &self.path
808    }
809
810    /// Get the hi-block table if present (v2+ archives)
811    pub fn hi_block_table(&self) -> Option<&HiBlockTable> {
812        self.hi_block_table.as_ref()
813    }
814
815    /// Validate MD5 checksums for v4 archives
816    fn validate_v4_md5_checksums(&mut self) -> Result<Option<Md5Status>> {
817        use md5::{Digest, Md5};
818
819        let v4_data = match &self.header.v4_data {
820            Some(data) => data,
821            None => return Ok(None),
822        };
823
824        // Helper function to calculate MD5 of raw table data
825        let mut validate_table_md5 = |expected: &[u8; 16],
826                                      offset: u64,
827                                      size: u64|
828         -> Result<bool> {
829            if size == 0 {
830                return Ok(true); // Empty table is valid
831            }
832
833            // Read raw table data
834            self.reader
835                .seek(SeekFrom::Start(self.archive_offset + offset))?;
836            let mut table_data = vec![0u8; size as usize];
837            match self.reader.read_exact(&mut table_data) {
838                Ok(_) => {
839                    // Calculate MD5
840                    let mut hasher = Md5::new();
841                    hasher.update(&table_data);
842                    let actual_md5: [u8; 16] = hasher.finalize().into();
843
844                    Ok(actual_md5 == *expected)
845                }
846                Err(e) => {
847                    log::warn!(
848                        "Failed to read table data for MD5 validation at offset 0x{:X}, size {}: {}",
849                        self.archive_offset + offset,
850                        size,
851                        e
852                    );
853                    Ok(false)
854                }
855            }
856        };
857
858        // Validate hash table MD5
859        let hash_table_valid = if self.header.hash_table_size > 0 {
860            let hash_offset = self.header.get_hash_table_pos();
861            let hash_size = v4_data.hash_table_size_64;
862            validate_table_md5(&v4_data.md5_hash_table, hash_offset, hash_size)?
863        } else {
864            true // No hash table to validate
865        };
866
867        // Validate block table MD5
868        let block_table_valid = if self.header.block_table_size > 0 {
869            let block_offset = self.header.get_block_table_pos();
870            let block_size = v4_data.block_table_size_64;
871            validate_table_md5(&v4_data.md5_block_table, block_offset, block_size)?
872        } else {
873            true // No block table to validate
874        };
875
876        // Validate hi-block table MD5 (if present)
877        let hi_block_table_valid = if let Some(hi_pos) = self.header.hi_block_table_pos {
878            if hi_pos != 0 {
879                let hi_size = v4_data.hi_block_table_size_64;
880                validate_table_md5(&v4_data.md5_hi_block_table, hi_pos, hi_size)?
881            } else {
882                true
883            }
884        } else {
885            true // No hi-block table
886        };
887
888        // Validate HET table MD5 (if present)
889        let het_table_valid = if let Some(het_pos) = self.header.het_table_pos {
890            if het_pos != 0 {
891                let het_size = v4_data.het_table_size_64;
892                validate_table_md5(&v4_data.md5_het_table, het_pos, het_size)?
893            } else {
894                true
895            }
896        } else {
897            true // No HET table
898        };
899
900        // Validate BET table MD5 (if present)
901        let bet_table_valid = if let Some(bet_pos) = self.header.bet_table_pos {
902            if bet_pos != 0 {
903                let bet_size = v4_data.bet_table_size_64;
904                validate_table_md5(&v4_data.md5_bet_table, bet_pos, bet_size)?
905            } else {
906                true
907            }
908        } else {
909            true // No BET table
910        };
911
912        // Validate header MD5 (first 192 bytes of header, excluding the MD5 field itself)
913        let header_valid = {
914            self.reader.seek(SeekFrom::Start(self.archive_offset))?;
915            let mut header_data = vec![0u8; 192];
916            match self.reader.read_exact(&mut header_data) {
917                Ok(_) => {
918                    let mut hasher = Md5::new();
919                    hasher.update(&header_data);
920                    let actual_md5: [u8; 16] = hasher.finalize().into();
921
922                    actual_md5 == v4_data.md5_mpq_header
923                }
924                Err(e) => {
925                    log::warn!("Failed to read header for MD5 validation: {e}");
926                    false
927                }
928            }
929        };
930
931        Ok(Some(Md5Status {
932            hash_table_valid,
933            block_table_valid,
934            hi_block_table_valid,
935            het_table_valid,
936            bet_table_valid,
937            header_valid,
938        }))
939    }
940
941    /// Get detailed information about the archive
942    pub fn get_info(&mut self) -> Result<ArchiveInfo> {
943        log::debug!("Getting archive info");
944
945        // Ensure tables are loaded
946        if self.hash_table.is_none() && self.het_table.is_none() {
947            log::debug!("Loading tables for info");
948            self.load_tables()?;
949        }
950
951        // Get file size
952        log::debug!("Getting file size");
953        let file_size = self.reader.get_ref().metadata()?.len();
954
955        // Count files
956        let file_count = if let Some(bet) = &self.bet_table {
957            bet.header.file_count as usize
958        } else if let Some(block_table) = &self.block_table {
959            // Count non-empty entries in block table
960            block_table
961                .entries()
962                .iter()
963                .filter(|entry| entry.file_size != 0)
964                .count()
965        } else {
966            0
967        };
968
969        // Get max file count
970        let max_file_count = if let Some(het) = &self.het_table {
971            het.header.max_file_count
972        } else {
973            self.header.hash_table_size
974        };
975
976        // Check for special files
977        let has_listfile = self.find_file("(listfile)")?.is_some();
978        let has_signature = self.find_file("(signature)")?.is_some();
979        let has_attributes = self.attributes.is_some() || self.find_file("(attributes)")?.is_some();
980
981        // Determine encryption status
982        let is_encrypted = if let Some(block_table) = &self.block_table {
983            use crate::tables::BlockEntry;
984            block_table
985                .entries()
986                .iter()
987                .any(|entry| (entry.flags & BlockEntry::FLAG_ENCRYPTED) != 0)
988        } else {
989            false
990        };
991
992        // Verify signature if present
993        let signature_status = if has_signature {
994            match self.verify_signature() {
995                Ok(status) => status,
996                Err(e) => {
997                    log::warn!("Failed to verify signature: {e}");
998                    SignatureStatus::WeakInvalid
999                }
1000            }
1001        } else {
1002            SignatureStatus::None
1003        };
1004
1005        // Build table info
1006        let hash_table_info = TableInfo {
1007            size: Some(self.header.hash_table_size),
1008            offset: self.header.get_hash_table_pos(),
1009            compressed_size: self.header.v4_data.as_ref().map(|v4| v4.hash_table_size_64),
1010            failed_to_load: self.hash_table.is_none() && self.header.hash_table_size > 0,
1011        };
1012
1013        let block_table_info = TableInfo {
1014            size: Some(self.header.block_table_size),
1015            offset: self.header.get_block_table_pos(),
1016            compressed_size: self
1017                .header
1018                .v4_data
1019                .as_ref()
1020                .map(|v4| v4.block_table_size_64),
1021            failed_to_load: self.block_table.is_none() && self.header.block_table_size > 0,
1022        };
1023
1024        let het_table_info = self.header.het_table_pos.and_then(|pos| {
1025            if pos == 0 {
1026                return None;
1027            }
1028
1029            // For v4, use the size from v4 data
1030            let mut compressed_size = self.header.v4_data.as_ref().map(|v4| v4.het_table_size_64);
1031
1032            // For v3 without v4 data, try to determine the size
1033            if compressed_size.is_none() && self.header.format_version == header::FormatVersion::V3
1034            {
1035                // Make a copy of the reader to avoid interfering with the main archive
1036                if let Ok(temp_reader) =
1037                    std::fs::File::open(&self.path).map(std::io::BufReader::new)
1038                {
1039                    let mut temp_archive = Self {
1040                        path: self.path.clone(),
1041                        reader: temp_reader,
1042                        archive_offset: self.archive_offset,
1043                        user_data: self.user_data.clone(),
1044                        header: self.header.clone(),
1045                        hash_table: None,
1046                        block_table: None,
1047                        hi_block_table: None,
1048                        het_table: None,
1049                        bet_table: None,
1050                        attributes: None,
1051                    };
1052
1053                    if let Ok(size) = temp_archive.read_het_table_size(pos) {
1054                        compressed_size = Some(size);
1055                    }
1056                }
1057            }
1058
1059            Some(TableInfo {
1060                size: self.het_table.as_ref().map(|het| het.header.max_file_count),
1061                offset: pos,
1062                compressed_size,
1063                failed_to_load: self.het_table.is_none(),
1064            })
1065        });
1066
1067        let bet_table_info = self.header.bet_table_pos.and_then(|pos| {
1068            if pos == 0 {
1069                return None;
1070            }
1071
1072            // For v4, use the size from v4 data
1073            let mut compressed_size = self.header.v4_data.as_ref().map(|v4| v4.bet_table_size_64);
1074
1075            // For v3 without v4 data, try to determine the size
1076            if compressed_size.is_none() && self.header.format_version == header::FormatVersion::V3
1077            {
1078                // Make a copy of the reader to avoid interfering with the main archive
1079                if let Ok(temp_reader) =
1080                    std::fs::File::open(&self.path).map(std::io::BufReader::new)
1081                {
1082                    let mut temp_archive = Self {
1083                        path: self.path.clone(),
1084                        reader: temp_reader,
1085                        archive_offset: self.archive_offset,
1086                        user_data: self.user_data.clone(),
1087                        header: self.header.clone(),
1088                        hash_table: None,
1089                        block_table: None,
1090                        hi_block_table: None,
1091                        het_table: None,
1092                        bet_table: None,
1093                        attributes: None,
1094                    };
1095
1096                    if let Ok(size) = temp_archive.read_bet_table_size(pos) {
1097                        compressed_size = Some(size);
1098                    }
1099                }
1100            }
1101
1102            Some(TableInfo {
1103                size: self.bet_table.as_ref().map(|bet| bet.header.file_count),
1104                offset: pos,
1105                compressed_size,
1106                failed_to_load: self.bet_table.is_none(),
1107            })
1108        });
1109
1110        let hi_block_table_info = self.header.hi_block_table_pos.and_then(|pos| {
1111            if pos == 0 {
1112                return None;
1113            }
1114
1115            Some(TableInfo {
1116                size: if self.hi_block_table.is_some() {
1117                    Some(self.header.block_table_size)
1118                } else {
1119                    None
1120                },
1121                offset: pos,
1122                compressed_size: self
1123                    .header
1124                    .v4_data
1125                    .as_ref()
1126                    .map(|v4| v4.hi_block_table_size_64),
1127                failed_to_load: self.hi_block_table.is_none(),
1128            })
1129        });
1130
1131        let user_data_info = self.user_data.as_ref().map(|ud| UserDataInfo {
1132            header_size: ud.user_data_header_size,
1133            data_size: ud.user_data_size,
1134        });
1135
1136        // MD5 verification for v4 archives
1137        let md5_status = if self.header.v4_data.is_some() {
1138            self.validate_v4_md5_checksums()?
1139        } else {
1140            None
1141        };
1142
1143        Ok(ArchiveInfo {
1144            path: self.path.clone(),
1145            file_size,
1146            archive_offset: self.archive_offset,
1147            format_version: self.header.format_version,
1148            file_count,
1149            max_file_count,
1150            sector_size: self.header.sector_size(),
1151            is_encrypted,
1152            has_signature,
1153            signature_status,
1154            hash_table_info,
1155            block_table_info,
1156            het_table_info,
1157            bet_table_info,
1158            hi_block_table_info,
1159            has_attributes,
1160            has_listfile,
1161            user_data_info,
1162            md5_status,
1163        })
1164    }
1165
1166    /// Get the hash table
1167    pub fn hash_table(&self) -> Option<&HashTable> {
1168        self.hash_table.as_ref()
1169    }
1170
1171    /// Get the block table
1172    pub fn block_table(&self) -> Option<&BlockTable> {
1173        self.block_table.as_ref()
1174    }
1175
1176    /// Get HET table reference
1177    pub fn het_table(&self) -> Option<&HetTable> {
1178        self.het_table.as_ref()
1179    }
1180
1181    /// Get BET table reference
1182    pub fn bet_table(&self) -> Option<&BetTable> {
1183        self.bet_table.as_ref()
1184    }
1185
1186    /// Find a file in the archive
1187    pub fn find_file(&self, filename: &str) -> Result<Option<FileInfo>> {
1188        // Check if this is a special file that should be searched in both table types
1189        let is_special_file = matches!(
1190            filename,
1191            "(listfile)" | "(attributes)" | "(signature)" | "(patch_metadata)"
1192        );
1193
1194        // For v3+ archives, prioritize HET/BET tables if they exist and are valid
1195        if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table) {
1196            // Check if tables have actual entries
1197            if het.header.max_file_count > 0 && bet.header.file_count > 0 {
1198                let (_file_index_opt, collision_candidates) =
1199                    het.find_file_with_collision_info(filename);
1200
1201                // HET uses 8-bit hashes which naturally have many collisions.
1202                // We must verify each candidate against the full BET hash to find the correct file.
1203                if !collision_candidates.is_empty() {
1204                    if collision_candidates.len() > 1 {
1205                        log::debug!(
1206                            "HET: '{}' has {} collision candidates, verifying against BET hashes",
1207                            filename,
1208                            collision_candidates.len()
1209                        );
1210                    }
1211
1212                    // Check each collision candidate against BET hash
1213                    for &candidate_index in &collision_candidates {
1214                        // Verify this candidate has the correct BET hash
1215                        if bet.verify_file_hash(candidate_index, filename) {
1216                            // Found the correct file - return its info
1217                            if let Some(bet_info) = bet.get_file_info(candidate_index) {
1218                                log::debug!(
1219                                    "HET/BET: Found '{}' at file_index={} (verified by BET hash)",
1220                                    filename,
1221                                    candidate_index
1222                                );
1223                                return Ok(Some(FileInfo {
1224                                    filename: filename.to_string(),
1225                                    hash_index: 0, // Not applicable for HET/BET
1226                                    block_index: candidate_index as usize,
1227                                    file_pos: self.archive_offset + bet_info.file_pos,
1228                                    compressed_size: bet_info.compressed_size,
1229                                    file_size: bet_info.file_size,
1230                                    flags: bet_info.flags,
1231                                    locale: 0, // HET/BET don't store locale separately
1232                                }));
1233                            }
1234                        }
1235                    }
1236
1237                    // No candidate matched - file not found in HET/BET
1238                    log::debug!(
1239                        "HET/BET: '{}' not found - {} candidates checked, none matched BET hash",
1240                        filename,
1241                        collision_candidates.len()
1242                    );
1243                }
1244
1245                // For special files, always check hash/block tables as fallback
1246                // For regular files, only fall back if hash tables exist
1247                if !is_special_file && (self.hash_table.is_none() || self.block_table.is_none()) {
1248                    return Ok(None);
1249                }
1250            }
1251        }
1252
1253        // Fall back to traditional hash/block tables if:
1254        // 1. HET/BET tables don't exist
1255        // 2. HET/BET tables are empty/invalid
1256        // 3. File wasn't found in HET/BET but we're looking for a special file
1257        // 4. File wasn't found in HET/BET but hash/block tables exist
1258        self.find_file_classic(filename)
1259    }
1260
1261    /// Classic file lookup using hash/block tables
1262    fn find_file_classic(&self, filename: &str) -> Result<Option<FileInfo>> {
1263        // If tables aren't loaded, return None instead of error
1264        // This is common for V3+ archives that only have HET/BET tables
1265        let hash_table = match self.hash_table.as_ref() {
1266            Some(table) => table,
1267            None => return Ok(None),
1268        };
1269        let block_table = match self.block_table.as_ref() {
1270            Some(table) => table,
1271            None => return Ok(None),
1272        };
1273
1274        // Try to find the file with default locale
1275        if let Some((hash_index, hash_entry)) = hash_table.find_file(filename, 0) {
1276            let block_entry = block_table
1277                .get(hash_entry.block_index as usize)
1278                .ok_or_else(|| Error::block_table("Invalid block index"))?;
1279
1280            // Calculate full file position for v2+ archives
1281            let file_pos = if let Some(hi_block) = &self.hi_block_table {
1282                let high_bits = hi_block.get_file_pos_high(hash_entry.block_index as usize);
1283                (high_bits << 32) | (block_entry.file_pos as u64)
1284            } else {
1285                block_entry.file_pos as u64
1286            };
1287
1288            Ok(Some(FileInfo {
1289                filename: filename.to_string(),
1290                hash_index,
1291                block_index: hash_entry.block_index as usize,
1292                file_pos: self.archive_offset + file_pos,
1293                compressed_size: block_entry.compressed_size as u64,
1294                file_size: block_entry.file_size as u64,
1295                flags: block_entry.flags,
1296                locale: hash_entry.locale,
1297            }))
1298        } else {
1299            Ok(None)
1300        }
1301    }
1302
1303    /// List files in the archive
1304    pub fn list(&mut self) -> Result<Vec<FileEntry>> {
1305        // Try to find and read (listfile)
1306        if let Some(_listfile_info) = self.find_file("(listfile)")? {
1307            // Try to read the listfile
1308            match self.read_file("(listfile)") {
1309                Ok(listfile_data) => {
1310                    // Parse the listfile
1311                    match special_files::parse_listfile(&listfile_data) {
1312                        Ok(filenames) => {
1313                            let mut entries = Vec::new();
1314
1315                            // Look up each file
1316                            for filename in filenames {
1317                                if let Some(file_info) = self.find_file(&filename)? {
1318                                    entries.push(FileEntry {
1319                                        name: filename,
1320                                        size: file_info.file_size,
1321                                        compressed_size: file_info.compressed_size,
1322                                        flags: file_info.flags,
1323                                        hashes: None,
1324                                        table_indices: Some((
1325                                            file_info.hash_index,
1326                                            Some(file_info.block_index),
1327                                        )),
1328                                    });
1329                                } else {
1330                                    // File is in listfile but not found in archive
1331                                    log::warn!(
1332                                        "File '{filename}' listed in (listfile) but not found in archive"
1333                                    );
1334                                }
1335                            }
1336
1337                            return Ok(entries);
1338                        }
1339                        Err(e) => {
1340                            log::warn!(
1341                                "Failed to parse (listfile): {e}. Falling back to anonymous enumeration."
1342                            );
1343                        }
1344                    }
1345                }
1346                Err(e) => {
1347                    log::warn!(
1348                        "Failed to read (listfile): {e}. Falling back to anonymous enumeration."
1349                    );
1350                }
1351            }
1352        }
1353
1354        // No listfile or failed to read/parse it, we'll need to enumerate entries without names
1355        log::info!("Enumerating anonymous entries");
1356
1357        let mut entries = Vec::new();
1358
1359        // For v3+ archives, prioritize HET/BET tables if they exist and are valid
1360        if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table)
1361            && het.header.max_file_count > 0
1362            && bet.header.file_count > 0
1363        {
1364            log::info!("Enumerating files using HET/BET tables");
1365
1366            // Enumerate using BET table
1367            for i in 0..bet.header.file_count {
1368                if let Some(bet_info) = bet.get_file_info(i) {
1369                    // Only include files that actually exist
1370                    if bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0 {
1371                        entries.push(FileEntry {
1372                            name: format!("file_{i:08}.dat"), // Unknown name with file index
1373                            size: bet_info.file_size,
1374                            compressed_size: bet_info.compressed_size,
1375                            flags: bet_info.flags,
1376                            hashes: None,
1377                            table_indices: Some((i as usize, None)), // file_index for HET/BET tables
1378                        });
1379                    }
1380                }
1381            }
1382
1383            // If we enumerated from HET/BET successfully, return early
1384            if !entries.is_empty() {
1385                return Ok(entries);
1386            }
1387        }
1388
1389        // Fall back to classic hash/block tables
1390        let hash_table = self
1391            .hash_table
1392            .as_ref()
1393            .ok_or_else(|| Error::invalid_format("No tables loaded for enumeration"))?;
1394        let block_table = self
1395            .block_table
1396            .as_ref()
1397            .ok_or_else(|| Error::invalid_format("No block table loaded"))?;
1398
1399        log::info!("Enumerating files using hash/block tables");
1400
1401        // Scan hash table for valid entries
1402        for (i, hash_entry) in hash_table.entries().iter().enumerate() {
1403            if hash_entry.is_valid()
1404                && let Some(block_entry) = block_table.get(hash_entry.block_index as usize)
1405                && block_entry.exists()
1406            {
1407                entries.push(FileEntry {
1408                    name: format!("file_{i:08}.dat"), // Unknown name with hash index
1409                    size: block_entry.file_size as u64,
1410                    compressed_size: block_entry.compressed_size as u64,
1411                    flags: block_entry.flags,
1412                    hashes: None,
1413                    table_indices: Some((i, Some(hash_entry.block_index as usize))), // hash_index, block_index
1414                });
1415            }
1416        }
1417
1418        Ok(entries)
1419    }
1420
1421    /// List all files in the archive by enumerating tables
1422    /// This shows all entries, using generic names for files not in listfile
1423    pub fn list_all(&mut self) -> Result<Vec<FileEntry>> {
1424        let mut entries = Vec::new();
1425
1426        // For v3+ archives, prioritize HET/BET tables if they exist and are valid
1427        if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table)
1428            && het.header.max_file_count > 0
1429            && bet.header.file_count > 0
1430        {
1431            log::info!("Enumerating all files using HET/BET tables");
1432
1433            // Enumerate using BET table
1434            for i in 0..bet.header.file_count {
1435                if let Some(bet_info) = bet.get_file_info(i) {
1436                    // Only include files that actually exist
1437                    if bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0 {
1438                        entries.push(FileEntry {
1439                            name: format!("file_{i:08}.dat"), // Unknown name with file index
1440                            size: bet_info.file_size,
1441                            compressed_size: bet_info.compressed_size,
1442                            flags: bet_info.flags,
1443                            hashes: None,
1444                            table_indices: Some((i as usize, None)), // file_index for HET/BET tables
1445                        });
1446                    }
1447                }
1448            }
1449
1450            // If we enumerated from HET/BET successfully, return early
1451            if !entries.is_empty() {
1452                return Ok(entries);
1453            }
1454        }
1455
1456        // Fall back to classic hash/block tables
1457        let hash_table = self
1458            .hash_table
1459            .as_ref()
1460            .ok_or_else(|| Error::invalid_format("No tables loaded for enumeration"))?;
1461        let block_table = self
1462            .block_table
1463            .as_ref()
1464            .ok_or_else(|| Error::invalid_format("No block table loaded"))?;
1465
1466        log::info!("Enumerating all files using hash/block tables");
1467
1468        // Enumerate all hash table entries
1469        let mut block_indices_seen = std::collections::HashSet::new();
1470
1471        for hash_entry in hash_table.entries().iter() {
1472            if hash_entry.is_valid() {
1473                let block_index = hash_entry.block_index as usize;
1474
1475                // Skip if we've already seen this block index (collision chain)
1476                if !block_indices_seen.insert(block_index) {
1477                    continue;
1478                }
1479
1480                if let Some(block_entry) = block_table.get(block_index)
1481                    && block_entry.exists()
1482                {
1483                    entries.push(FileEntry {
1484                        name: format!("file_{block_index:08}.dat"),
1485                        size: block_entry.file_size as u64,
1486                        compressed_size: block_entry.compressed_size as u64,
1487                        flags: block_entry.flags,
1488                        hashes: None,
1489                        table_indices: Some((0, Some(block_index))), // Use 0 for hash_index since we don't track it here
1490                    });
1491                }
1492            }
1493        }
1494
1495        // Sort by block index (which is embedded in the generated names)
1496        entries.sort_by(|a, b| a.name.cmp(&b.name));
1497
1498        Ok(entries)
1499    }
1500
1501    /// List files in the archive with hash information
1502    pub fn list_with_hashes(&mut self) -> Result<Vec<FileEntry>> {
1503        let mut entries = self.list()?;
1504
1505        // Calculate hashes for each entry
1506        for entry in &mut entries {
1507            let hash1 = crate::crypto::hash_string(&entry.name, crate::crypto::hash_type::NAME_A);
1508            let hash2 = crate::crypto::hash_string(&entry.name, crate::crypto::hash_type::NAME_B);
1509            entry.hashes = Some((hash1, hash2));
1510        }
1511
1512        Ok(entries)
1513    }
1514
1515    /// List all files in the archive by enumerating tables with hash information
1516    pub fn list_all_with_hashes(&mut self) -> Result<Vec<FileEntry>> {
1517        let mut entries = Vec::new();
1518
1519        // For v3+ archives, use HET/BET tables
1520        if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table)
1521            && het.header.max_file_count > 0
1522            && bet.header.file_count > 0
1523        {
1524            log::info!("Enumerating all files using HET/BET tables with hashes");
1525
1526            // Enumerate using BET table
1527            for i in 0..bet.header.file_count {
1528                if let Some(bet_info) = bet.get_file_info(i)
1529                    && bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0
1530                {
1531                    entries.push(FileEntry {
1532                        name: format!("file_{i:08}.dat"),
1533                        size: bet_info.file_size,
1534                        compressed_size: bet_info.compressed_size,
1535                        flags: bet_info.flags,
1536                        hashes: None, // HET/BET doesn't expose name hashes directly
1537                        table_indices: Some((i as usize, None)), // file_index for HET/BET tables
1538                    });
1539                }
1540            }
1541
1542            if !entries.is_empty() {
1543                return Ok(entries);
1544            }
1545        }
1546
1547        // Fall back to classic hash/block tables
1548        let hash_table = self
1549            .hash_table
1550            .as_ref()
1551            .ok_or_else(|| Error::invalid_format("No tables loaded for enumeration"))?;
1552        let block_table = self
1553            .block_table
1554            .as_ref()
1555            .ok_or_else(|| Error::invalid_format("No block table loaded"))?;
1556
1557        log::info!("Enumerating all files using hash/block tables with hashes");
1558
1559        // Enumerate all hash table entries - here we can get the actual hashes!
1560        let mut block_indices_seen = std::collections::HashSet::new();
1561
1562        for hash_entry in hash_table.entries().iter() {
1563            if hash_entry.is_valid() {
1564                let block_index = hash_entry.block_index as usize;
1565
1566                if !block_indices_seen.insert(block_index) {
1567                    continue;
1568                }
1569
1570                if let Some(block_entry) = block_table.get(block_index)
1571                    && block_entry.exists()
1572                {
1573                    entries.push(FileEntry {
1574                        name: format!("file_{block_index:08}.dat"),
1575                        size: block_entry.file_size as u64,
1576                        compressed_size: block_entry.compressed_size as u64,
1577                        flags: block_entry.flags,
1578                        hashes: Some((hash_entry.name_1, hash_entry.name_2)),
1579                        table_indices: Some((0, Some(block_index))), // Use 0 for hash_index since we don't track it here
1580                    });
1581                }
1582            }
1583        }
1584
1585        // Sort by block index
1586        entries.sort_by(|a, b| a.name.cmp(&b.name));
1587
1588        Ok(entries)
1589    }
1590
1591    /// Read a file from the archive
1592    pub fn read_file(&mut self, name: &str) -> Result<Vec<u8>> {
1593        let file_info = self
1594            .find_file(name)?
1595            .ok_or_else(|| Error::FileNotFound(name.to_string()))?;
1596
1597        // Check if this is a patch file - patch files cannot be read directly
1598        if file_info.is_patch_file() {
1599            return Err(Error::OperationNotSupported {
1600                version: self.header.format_version as u16,
1601                operation: format!(
1602                    "Reading patch file '{name}' directly. Patch files contain binary patches that must be applied to base files."
1603                ),
1604            });
1605        }
1606
1607        // For v3+ archives with HET/BET tables, we already have all the info we need in FileInfo
1608        // For classic archives, we need to get additional info from the block table
1609        let (file_size_for_key, actual_file_size) =
1610            if self.het_table.is_some() && self.bet_table.is_some() {
1611                // Using HET/BET tables - FileInfo already has all the data
1612                (file_info.file_size as u32, file_info.file_size)
1613            } else {
1614                // Using classic tables - need block entry for accurate sizes
1615                let block_table = self
1616                    .block_table
1617                    .as_ref()
1618                    .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
1619                let block_entry = block_table
1620                    .get(file_info.block_index)
1621                    .ok_or_else(|| Error::block_table("Invalid block index"))?;
1622                (block_entry.file_size, block_entry.file_size as u64)
1623            };
1624
1625        // Calculate encryption key if needed
1626        let key = if file_info.is_encrypted() {
1627            let base_key = hash_string(name, hash_type::FILE_KEY);
1628            if file_info.has_fix_key() {
1629                // Apply FIX_KEY modification
1630                let file_pos = (file_info.file_pos - self.archive_offset) as u32;
1631                (base_key.wrapping_add(file_pos)) ^ file_size_for_key
1632            } else {
1633                base_key
1634            }
1635        } else {
1636            0
1637        };
1638
1639        // Read the file data
1640        self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
1641
1642        if file_info.is_single_unit() || !file_info.is_compressed() {
1643            // Single unit or uncompressed file - read directly
1644            let mut data = vec![0u8; file_info.compressed_size as usize];
1645            self.reader.read_exact(&mut data)?;
1646
1647            // Decrypt if needed
1648            if file_info.is_encrypted() {
1649                log::debug!(
1650                    "Decrypting file data: key=0x{:08X}, size={}",
1651                    key,
1652                    data.len()
1653                );
1654                if data.len() <= 64 {
1655                    log::debug!("Before decrypt: {:02X?}", &data);
1656                }
1657                decrypt_file_data(&mut data, key);
1658                if data.len() <= 64 {
1659                    log::debug!("After decrypt: {:02X?}", &data);
1660                }
1661            }
1662
1663            // Validate CRC if present for single unit files
1664            if file_info.has_sector_crc() && file_info.is_single_unit() {
1665                // For single unit files, there's one CRC after the data
1666                let mut crc_bytes = [0u8; 4];
1667                self.reader.read_exact(&mut crc_bytes)?;
1668                let expected_crc = u32::from_le_bytes(crc_bytes);
1669
1670                // CRC is calculated on the decompressed data
1671                let data_to_check = if file_info.is_compressed() {
1672                    // We need to decompress first to check CRC
1673                    let compression_type = data[0];
1674                    let compressed_data = &data[1..];
1675                    compression::decompress(
1676                        compressed_data,
1677                        compression_type,
1678                        actual_file_size as usize,
1679                    )?
1680                } else {
1681                    data.clone()
1682                };
1683
1684                // MPQ uses ADLER32 for sector checksums, not CRC32 despite the name
1685                let actual_crc = adler2::adler32_slice(&data_to_check);
1686                if actual_crc != expected_crc {
1687                    return Err(Error::ChecksumMismatch {
1688                        file: name.to_string(),
1689                        expected: expected_crc,
1690                        actual: actual_crc,
1691                    });
1692                }
1693
1694                log::debug!("Single unit file CRC validated: 0x{actual_crc:08X}");
1695            }
1696
1697            // Decompress if needed
1698            if file_info.is_compressed() {
1699                if file_info.is_single_unit() {
1700                    // SINGLE_UNIT files: Get compression method from block table flags
1701                    // NO compression type byte prefix in the data
1702
1703                    // Special case: If compressed_size == file_size, the file might be stored uncompressed
1704                    // despite having the COMPRESS flag set
1705                    if data.len() == actual_file_size as usize {
1706                        log::debug!(
1707                            "SINGLE_UNIT file has equal compressed/uncompressed size ({} bytes), trying uncompressed first",
1708                            data.len()
1709                        );
1710
1711                        // Try treating as uncompressed data first
1712                        // This handles cases where the COMPRESS flag is set but data is actually uncompressed
1713                        Ok(data)
1714                    } else if let Some(compression_method) = file_info.get_compression_method() {
1715                        // SINGLE_UNIT files DO have compression method byte prefix!
1716                        // This was our bug - we thought they didn't
1717                        if !data.is_empty() {
1718                            let actual_compression_method = data[0];
1719                            let compressed_data = &data[1..];
1720
1721                            log::debug!(
1722                                "Decompressing SINGLE_UNIT file: method_from_flags=0x{:02X}, actual_method_byte=0x{:02X}, compressed_size={}, expected_size={}",
1723                                compression_method,
1724                                actual_compression_method,
1725                                compressed_data.len(),
1726                                actual_file_size
1727                            );
1728
1729                            // Use the actual compression method from the data, not from flags
1730                            // This ensures we handle multi-compression correctly
1731                            compression::decompress(
1732                                compressed_data,
1733                                actual_compression_method,
1734                                actual_file_size as usize,
1735                            )
1736                        } else {
1737                            Err(Error::compression("Empty compressed data"))
1738                        }
1739                    } else {
1740                        Err(Error::compression(
1741                            "Could not determine compression method from flags",
1742                        ))
1743                    }
1744                } else {
1745                    // SECTORED files: Should not reach here for single-unit code path
1746                    // This will be handled in read_sectored_file()
1747                    log::warn!("Non-single-unit compressed file in single-unit code path");
1748                    Ok(data)
1749                }
1750            } else {
1751                // For encrypted files, trim to original file size to remove padding
1752                if file_info.is_encrypted() && data.len() > actual_file_size as usize {
1753                    data.truncate(actual_file_size as usize);
1754                }
1755                Ok(data)
1756            }
1757        } else {
1758            // Multi-sector compressed file
1759            self.read_sectored_file(&file_info, key)
1760        }
1761    }
1762
1763    /// Read raw patch file data
1764    ///
1765    /// This method reads patch files (files with MPQ_FILE_PATCH_FILE flag) without
1766    /// rejecting them. It returns the raw PTCH format data that can be parsed and
1767    /// applied to base files.
1768    ///
1769    /// This is used internally by PatchChain to read patch files for application.
1770    pub(crate) fn read_patch_file_raw(&mut self, name: &str) -> Result<Vec<u8>> {
1771        let file_info = self
1772            .find_file(name)?
1773            .ok_or_else(|| Error::FileNotFound(name.to_string()))?;
1774
1775        // Verify this is actually a patch file
1776        if !file_info.is_patch_file() {
1777            return Err(Error::invalid_format(format!(
1778                "File '{name}' is not a patch file"
1779            )));
1780        }
1781
1782        // For v3+ archives with HET/BET tables, we already have all the info we need in FileInfo
1783        // For classic archives, we need to get additional info from the block table
1784        let (file_size_for_key, _actual_file_size) =
1785            if self.het_table.is_some() && self.bet_table.is_some() {
1786                // Using HET/BET tables - FileInfo already has all the data
1787                (file_info.file_size as u32, file_info.file_size)
1788            } else {
1789                // Using classic tables - need block entry for accurate sizes
1790                let block_table = self
1791                    .block_table
1792                    .as_ref()
1793                    .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
1794                let block_entry = block_table
1795                    .get(file_info.block_index)
1796                    .ok_or_else(|| Error::block_table("Invalid block index"))?;
1797                (block_entry.file_size, block_entry.file_size as u64)
1798            };
1799
1800        // Calculate encryption key if needed
1801        let key = if file_info.is_encrypted() {
1802            let base_key = hash_string(name, hash_type::FILE_KEY);
1803            if file_info.has_fix_key() {
1804                // Apply FIX_KEY modification
1805                let file_pos = (file_info.file_pos - self.archive_offset) as u32;
1806                (base_key.wrapping_add(file_pos)) ^ file_size_for_key
1807            } else {
1808                base_key
1809            }
1810        } else {
1811            0
1812        };
1813
1814        // Read the file data
1815        // Patch files start with TPatchInfo structure (uncompressed metadata)
1816        self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
1817
1818        // Read TPatchInfo header (28 bytes minimum)
1819        let mut patch_info_buf = [0u8; 28];
1820        self.reader.read_exact(&mut patch_info_buf)?;
1821
1822        let patch_info_length = u32::from_le_bytes([
1823            patch_info_buf[0],
1824            patch_info_buf[1],
1825            patch_info_buf[2],
1826            patch_info_buf[3],
1827        ]);
1828        let patch_info_flags = u32::from_le_bytes([
1829            patch_info_buf[4],
1830            patch_info_buf[5],
1831            patch_info_buf[6],
1832            patch_info_buf[7],
1833        ]);
1834        let patch_data_size = u32::from_le_bytes([
1835            patch_info_buf[8],
1836            patch_info_buf[9],
1837            patch_info_buf[10],
1838            patch_info_buf[11],
1839        ]);
1840
1841        log::debug!(
1842            "TPatchInfo: length={}, flags=0x{:08X}, data_size={} bytes",
1843            patch_info_length,
1844            patch_info_flags,
1845            patch_data_size
1846        );
1847
1848        // The actual decompressed size is patch_data_size, not file_info.file_size!
1849        let actual_patch_size = patch_data_size as usize;
1850
1851        // After TPatchInfo, check if file is sectored or single-unit
1852        // Patch files can be sectored despite lacking the SINGLE_UNIT flag
1853        let is_single_unit = file_info.is_single_unit();
1854
1855        if is_single_unit {
1856            log::debug!("Patch file is stored as single unit");
1857            let compressed_data_size =
1858                file_info.compressed_size as usize - patch_info_length as usize;
1859
1860            let mut data = vec![0u8; compressed_data_size];
1861            self.reader.read_exact(&mut data)?;
1862
1863            log::debug!(
1864                "Read {} bytes of compressed patch data (single unit)",
1865                data.len()
1866            );
1867            log::debug!("First 32 bytes: {:02X?}", &data[..32.min(data.len())]);
1868
1869            // Decrypt if needed (though patch files are typically not encrypted)
1870            if file_info.is_encrypted() {
1871                log::debug!(
1872                    "Decrypting patch file data: key=0x{:08X}, size={}",
1873                    key,
1874                    data.len()
1875                );
1876                decrypt_file_data(&mut data, key);
1877            }
1878
1879            // Decompress if needed
1880            if file_info.is_compressed() {
1881                let compression_type = data[0];
1882                let compressed_data = &data[1..];
1883
1884                log::debug!(
1885                    "Decompressing patch file (single unit): method=0x{:02X}, compressed={} bytes → {} bytes",
1886                    compression_type,
1887                    compressed_data.len(),
1888                    actual_patch_size
1889                );
1890
1891                compression::decompress(compressed_data, compression_type, actual_patch_size)
1892            } else {
1893                Ok(data)
1894            }
1895        } else {
1896            // Sectored patch file - read using sector table
1897            log::debug!("Patch file is sectored, reading with modified sector handling");
1898
1899            // Calculate sector count based on patch_data_size, not file_size
1900            let sector_size = self.header.sector_size();
1901            let sector_count = (patch_data_size as usize).div_ceil(sector_size);
1902
1903            log::debug!(
1904                "Patch sectors: data_size={}, sector_size={}, sector_count={}",
1905                patch_data_size,
1906                sector_size,
1907                sector_count
1908            );
1909
1910            // Read sector offset table
1911            let offset_table_size = (sector_count + 1) * 4;
1912            let mut offset_data = vec![0u8; offset_table_size];
1913            self.reader.read_exact(&mut offset_data)?;
1914
1915            log::debug!(
1916                "Read sector offset table: {} bytes for {} sectors",
1917                offset_table_size,
1918                sector_count
1919            );
1920
1921            // Parse sector offsets
1922            let mut sector_offsets = Vec::with_capacity(sector_count + 1);
1923            let mut cursor = std::io::Cursor::new(&offset_data);
1924            for _ in 0..=sector_count {
1925                sector_offsets.push(cursor.read_u32::<LittleEndian>()?);
1926            }
1927
1928            log::debug!("Sector offsets: {:?}", &sector_offsets);
1929
1930            // Read and decompress each sector
1931            let mut decompressed_data = Vec::with_capacity(patch_data_size as usize);
1932
1933            for i in 0..sector_count {
1934                let sector_start = sector_offsets[i] as usize;
1935                let sector_end = sector_offsets[i + 1] as usize;
1936                let sector_compressed_size = sector_end - sector_start;
1937
1938                log::debug!(
1939                    "Reading sector {}: offset={}, size={} bytes",
1940                    i,
1941                    sector_start,
1942                    sector_compressed_size
1943                );
1944
1945                // Sector offsets are relative to the START of the offset table, NOT after it
1946                // So we need to seek to: file_pos + TPatchInfo + sector_offset
1947                let sector_file_pos =
1948                    file_info.file_pos + patch_info_length as u64 + sector_start as u64;
1949
1950                self.reader.seek(SeekFrom::Start(sector_file_pos))?;
1951
1952                let mut sector_data = vec![0u8; sector_compressed_size];
1953                self.reader.read_exact(&mut sector_data)?;
1954
1955                log::debug!(
1956                    "Sector {} data first 16 bytes: {:02X?}",
1957                    i,
1958                    &sector_data[..16.min(sector_data.len())]
1959                );
1960
1961                // Patch file sectors use standard MPQ compression (Zlib/BZip2/etc)
1962                // First byte indicates compression method, remaining bytes are compressed PTCH data
1963                let compression_method = sector_data[0];
1964                log::debug!(
1965                    "Decompressing sector {} with method 0x{:02X} ({} bytes compressed)",
1966                    i,
1967                    compression_method,
1968                    sector_data.len() - 1
1969                );
1970
1971                // Decompress using standard MPQ decompression
1972                let expected_size =
1973                    sector_size.min(patch_data_size as usize - decompressed_data.len());
1974                let sector_decompressed = compression::decompress(
1975                    &sector_data[1..], // Skip compression method byte
1976                    compression_method,
1977                    expected_size,
1978                )?;
1979
1980                log::debug!(
1981                    "Sector {} decompressed to {} bytes",
1982                    i,
1983                    sector_decompressed.len()
1984                );
1985
1986                decompressed_data.extend_from_slice(&sector_decompressed);
1987            }
1988
1989            log::debug!(
1990                "Successfully decompressed {} bytes from {} sectors",
1991                decompressed_data.len(),
1992                sector_count
1993            );
1994
1995            Ok(decompressed_data)
1996        }
1997    }
1998
1999    /// Read a file by table indices (for files with generic names)
2000    pub fn read_file_by_indices(
2001        &mut self,
2002        hash_index: usize,
2003        block_index: Option<usize>,
2004    ) -> Result<Vec<u8>> {
2005        let file_info = if let Some(block_idx) = block_index {
2006            // Classic hash/block table access
2007            let hash_table = self
2008                .hash_table
2009                .as_ref()
2010                .ok_or_else(|| Error::invalid_format("Hash table not loaded"))?;
2011            let block_table = self
2012                .block_table
2013                .as_ref()
2014                .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
2015
2016            let hash_entry = hash_table
2017                .entries()
2018                .get(hash_index)
2019                .ok_or_else(|| Error::hash_table("Invalid hash index"))?;
2020            let block_entry = block_table
2021                .get(block_idx)
2022                .ok_or_else(|| Error::block_table("Invalid block index"))?;
2023
2024            // Calculate full file position for v2+ archives
2025            let file_pos = if let Some(hi_block) = &self.hi_block_table {
2026                let high_bits = hi_block.get_file_pos_high(block_idx);
2027                (high_bits << 32) | (block_entry.file_pos as u64)
2028            } else {
2029                block_entry.file_pos as u64
2030            };
2031
2032            FileInfo {
2033                filename: format!("file_{hash_index:08}.dat"),
2034                hash_index,
2035                block_index: block_idx,
2036                file_pos: self.archive_offset + file_pos,
2037                compressed_size: block_entry.compressed_size as u64,
2038                file_size: block_entry.file_size as u64,
2039                flags: block_entry.flags,
2040                locale: hash_entry.locale,
2041            }
2042        } else {
2043            // HET/BET table access (file_index is in hash_index parameter)
2044            let bet = self
2045                .bet_table
2046                .as_ref()
2047                .ok_or_else(|| Error::invalid_format("BET table not loaded"))?;
2048
2049            let bet_info = bet
2050                .get_file_info(hash_index as u32)
2051                .ok_or_else(|| Error::invalid_format("Invalid file index"))?;
2052
2053            // For HET/BET files, the file position is calculated differently
2054            let file_pos = self.archive_offset + bet_info.file_pos;
2055
2056            FileInfo {
2057                filename: format!("file_{hash_index:08}.dat"),
2058                hash_index: 0,  // Not meaningful for HET/BET
2059                block_index: 0, // Not meaningful for HET/BET
2060                file_pos,
2061                compressed_size: bet_info.compressed_size,
2062                file_size: bet_info.file_size,
2063                flags: bet_info.flags,
2064                locale: 0, // Not applicable for HET/BET
2065            }
2066        };
2067
2068        // Check if this is a patch file - patch files cannot be read directly
2069        if file_info.is_patch_file() {
2070            return Err(Error::OperationNotSupported {
2071                version: self.header.format_version as u16,
2072                operation: format!(
2073                    "Reading patch file '{}' directly. Patch files contain binary patches that must be applied to base files.",
2074                    file_info.filename
2075                ),
2076            });
2077        }
2078
2079        // Now use the existing file reading logic
2080        // For encrypted files, we need a key. Since we don't have the real filename,
2081        // we'll use a default key based on the table index
2082        let key = if file_info.is_encrypted() {
2083            // Use a generic key calculation for anonymous files
2084            hash_string(&file_info.filename, hash_type::FILE_KEY)
2085        } else {
2086            0
2087        };
2088
2089        // Continue with normal file reading logic based on whether it's sectored
2090        let (file_size_for_key, actual_file_size) =
2091            if self.het_table.is_some() && self.bet_table.is_some() {
2092                // Using HET/BET tables - FileInfo already has all the data
2093                (file_info.file_size as u32, file_info.file_size)
2094            } else {
2095                // Using classic tables - need block entry for accurate sizes
2096                let block_table = self
2097                    .block_table
2098                    .as_ref()
2099                    .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
2100                let block_entry = block_table
2101                    .get(file_info.block_index)
2102                    .ok_or_else(|| Error::block_table("Invalid block index"))?;
2103                (block_entry.file_size, block_entry.file_size as u64)
2104            };
2105
2106        // Adjust key for file size if needed
2107        let key = if file_info.is_encrypted() && file_info.has_fix_key() {
2108            key.wrapping_add(file_size_for_key)
2109        } else {
2110            key
2111        };
2112
2113        // Read the file data
2114        self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
2115
2116        if file_info.is_single_unit() || !file_info.is_compressed() {
2117            // Single unit or uncompressed file - read directly
2118            let mut data = vec![0u8; file_info.compressed_size as usize];
2119            self.reader.read_exact(&mut data)?;
2120
2121            // Decrypt if needed
2122            if file_info.is_encrypted() {
2123                log::debug!(
2124                    "Decrypting file data: key=0x{:08X}, size={}",
2125                    key,
2126                    data.len()
2127                );
2128                decrypt_file_data(&mut data, key);
2129            }
2130
2131            // Handle compression for single unit files
2132            if file_info.is_compressed() {
2133                if data.is_empty() {
2134                    return Err(Error::compression("File data is empty"));
2135                }
2136
2137                // Check if this is IMPLODE compression (no compression type prefix)
2138                if file_info.is_implode() {
2139                    log::debug!(
2140                        "Decompressing single unit IMPLODE file: input_size={}, target_size={}",
2141                        data.len(),
2142                        actual_file_size
2143                    );
2144                    compression::decompress(&data, 0x08, actual_file_size as usize)
2145                } else {
2146                    // COMPRESS flag - has compression type byte prefix
2147                    let compression_type = data[0];
2148                    let compressed_data = &data[1..];
2149
2150                    log::debug!(
2151                        "Decompressing single unit file: method=0x{:02X}, input_size={}, target_size={}, first bytes: {:02X?}",
2152                        compression_type,
2153                        compressed_data.len(),
2154                        actual_file_size,
2155                        &compressed_data[..compressed_data.len().min(16)]
2156                    );
2157
2158                    compression::decompress(
2159                        compressed_data,
2160                        compression_type,
2161                        actual_file_size as usize,
2162                    )
2163                }
2164            } else {
2165                Ok(data)
2166            }
2167        } else {
2168            // Multi-sector compressed file
2169            self.read_sectored_file(&file_info, key)
2170        }
2171    }
2172
2173    /// Read a file that is split into sectors
2174    fn read_sectored_file(&mut self, file_info: &FileInfo, key: u32) -> Result<Vec<u8>> {
2175        let sector_size = self.header.sector_size();
2176        let sector_count = (file_info.file_size as usize).div_ceil(sector_size);
2177
2178        log::debug!("Reading sectored file:");
2179        log::debug!("  file_size: {} bytes", file_info.file_size);
2180        log::debug!("  compressed_size: {} bytes", file_info.compressed_size);
2181        log::debug!("  sector_size: {} bytes", sector_size);
2182        log::debug!("  sector_count: {}", sector_count);
2183        log::debug!("  is_patch_file: {}", file_info.is_patch_file());
2184
2185        // Read sector offset table
2186        self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
2187        let offset_table_size = (sector_count + 1) * 4;
2188        log::debug!("  offset_table_size: {} bytes", offset_table_size);
2189        log::debug!(
2190            "  Attempting to read offset table at position 0x{:X}",
2191            file_info.file_pos
2192        );
2193
2194        let mut offset_data = vec![0u8; offset_table_size];
2195        self.reader.read_exact(&mut offset_data).map_err(|e| {
2196            log::error!("Failed to read offset table: {}", e);
2197            log::error!(
2198                "  Tried to read {} bytes at position 0x{:X}",
2199                offset_table_size,
2200                file_info.file_pos
2201            );
2202            e
2203        })?;
2204
2205        // Decrypt sector offset table if needed
2206        if file_info.is_encrypted() {
2207            let offset_key = key.wrapping_sub(1);
2208            decrypt_file_data(&mut offset_data, offset_key);
2209        }
2210
2211        // Parse sector offsets
2212        let mut sector_offsets = Vec::with_capacity(sector_count + 1);
2213        let mut cursor = std::io::Cursor::new(&offset_data);
2214        for _ in 0..=sector_count {
2215            sector_offsets.push(cursor.read_u32::<LittleEndian>()?);
2216        }
2217
2218        log::debug!(
2219            "Sector offsets: first={}, last={}",
2220            sector_offsets.first().copied().unwrap_or(0),
2221            sector_offsets.last().copied().unwrap_or(0)
2222        );
2223
2224        // Check if we have sector CRCs
2225        let mut sector_crcs = None;
2226        if file_info.has_sector_crc() {
2227            // The first sector offset tells us where the data starts
2228            // If it's large enough to accommodate a CRC table, then CRCs are present
2229            let first_data_offset = sector_offsets[0] as usize;
2230            let expected_crc_table_start = offset_table_size;
2231            let expected_crc_table_size = sector_count * 4;
2232
2233            if first_data_offset >= expected_crc_table_start + expected_crc_table_size {
2234                // CRC table follows the offset table
2235                let mut crc_data = vec![0u8; expected_crc_table_size];
2236                self.reader.read_exact(&mut crc_data)?;
2237
2238                // CRC table may be encrypted if the file is encrypted
2239                // According to MPQ format, CRC table uses the same key as the offset table but offset by sector count
2240                if file_info.is_encrypted() {
2241                    let crc_key = key.wrapping_sub(1).wrapping_add(sector_count as u32);
2242                    decrypt_file_data(&mut crc_data, crc_key);
2243                }
2244
2245                let mut crcs = Vec::with_capacity(sector_count);
2246                let mut cursor = std::io::Cursor::new(&crc_data);
2247                for _ in 0..sector_count {
2248                    crcs.push(cursor.read_u32::<LittleEndian>()?);
2249                }
2250
2251                // Log before moving
2252                log::debug!(
2253                    "Read {} sector CRCs, first few: {:?}",
2254                    sector_count,
2255                    &crcs[..5.min(crcs.len())]
2256                );
2257
2258                sector_crcs = Some(crcs);
2259            } else {
2260                log::debug!(
2261                    "File has SECTOR_CRC flag but insufficient space for CRC table (offset_table_size={}, first_data_offset={}, needed={}). This is common in some MPQ implementations.",
2262                    offset_table_size,
2263                    first_data_offset,
2264                    expected_crc_table_start + expected_crc_table_size
2265                );
2266            }
2267        }
2268
2269        // Read and decompress each sector
2270        let mut decompressed_data = Vec::with_capacity(file_info.file_size as usize);
2271
2272        // Pre-allocate a reusable buffer for sector reading
2273        // Add some overhead for compression headers
2274        let max_sector_size = sector_size + 1024;
2275        let mut sector_buffer = vec![0u8; max_sector_size];
2276
2277        for i in 0..sector_count {
2278            let sector_start = sector_offsets[i] as u64;
2279            let sector_end = sector_offsets[i + 1] as u64;
2280
2281            if sector_end < sector_start {
2282                // This can happen with corrupted or malformed archives
2283                // Try to recover by using the expected sector size
2284                log::warn!(
2285                    "Invalid sector offsets detected: start={sector_start}, end={sector_end} for sector {i}. Attempting recovery."
2286                );
2287
2288                // Skip this sector and continue with zeros
2289                let remaining = file_info.file_size as usize - decompressed_data.len();
2290                let expected_size = remaining.min(sector_size);
2291                decompressed_data.extend(vec![0u8; expected_size]);
2292                continue;
2293            }
2294
2295            let sector_size_compressed = (sector_end - sector_start) as usize;
2296
2297            // Calculate expected decompressed size for this sector
2298            let remaining = file_info.file_size as usize - decompressed_data.len();
2299            let expected_size = remaining.min(sector_size);
2300
2301            // Seek to sector data - offsets are absolute from file position
2302            self.reader
2303                .seek(SeekFrom::Start(file_info.file_pos + sector_start))?;
2304
2305            // Ensure our buffer is large enough
2306            if sector_size_compressed > sector_buffer.len() {
2307                sector_buffer.resize(sector_size_compressed, 0);
2308            }
2309
2310            // Read sector data into the reusable buffer
2311            let sector_data = &mut sector_buffer[..sector_size_compressed];
2312            self.reader.read_exact(sector_data)?;
2313
2314            if i == 0 {
2315                log::debug!(
2316                    "First sector: offset={}, size={}, first 16 bytes: {:02X?}",
2317                    sector_start,
2318                    sector_size_compressed,
2319                    &sector_data[..16.min(sector_data.len())]
2320                );
2321            }
2322
2323            // Decrypt sector if needed
2324            if file_info.is_encrypted() {
2325                let sector_key = key.wrapping_add(i as u32);
2326                decrypt_file_data(sector_data, sector_key);
2327            }
2328
2329            // Validate CRC if present - MUST be done AFTER decryption but BEFORE decompression
2330            // Skip CRC validation for now due to decryption key issues in some archives
2331            if let Some(ref _crcs) = sector_crcs {
2332                // Temporarily disabled CRC validation
2333                // TODO: Fix CRC decryption key calculation for proper validation
2334                log::trace!("Skipping CRC validation for sector {i}");
2335            }
2336
2337            // Decompress sector
2338            let decompressed_sector = if file_info.is_compressed()
2339                && sector_size_compressed < expected_size
2340            {
2341                if !sector_data.is_empty() {
2342                    // Check if this is IMPLODE compression (no compression type prefix)
2343                    if file_info.is_implode() {
2344                        // IMPLODE compression - no compression type byte prefix
2345                        match compression::decompress(sector_data, 0x08, expected_size) {
2346                            Ok(decompressed) => decompressed,
2347                            Err(e) => {
2348                                log::warn!(
2349                                    "Failed to decompress IMPLODE sector {i}: {e}. Using zeros."
2350                                );
2351                                vec![0u8; expected_size]
2352                            }
2353                        }
2354                    } else {
2355                        // COMPRESS flag - has compression type byte prefix
2356                        let compression_type = sector_data[0];
2357                        let compressed_data = &sector_data[1..];
2358                        match compression::decompress(
2359                            compressed_data,
2360                            compression_type,
2361                            expected_size,
2362                        ) {
2363                            Ok(decompressed) => decompressed,
2364                            Err(e) => {
2365                                log::warn!("Failed to decompress sector {i}: {e}. Using zeros.");
2366                                vec![0u8; expected_size]
2367                            }
2368                        }
2369                    }
2370                } else {
2371                    log::warn!("Empty compressed sector data for sector {i}. Using zeros.");
2372                    vec![0u8; expected_size]
2373                }
2374            } else {
2375                // Sector is not compressed
2376                sector_data[..expected_size.min(sector_data.len())].to_vec()
2377            };
2378
2379            decompressed_data.extend_from_slice(&decompressed_sector);
2380        }
2381
2382        Ok(decompressed_data)
2383    }
2384
2385    /// Load attributes from the (attributes) file if present
2386    pub fn load_attributes(&mut self) -> Result<()> {
2387        // Check if attributes are already loaded
2388        if self.attributes.is_some() {
2389            return Ok(());
2390        }
2391
2392        // Try to read the (attributes) file
2393        match self.read_file("(attributes)") {
2394            Ok(mut data) => {
2395                // Get block count for parsing
2396                // The attributes file should contain entries for all files in the archive,
2397                // including potentially itself (varies by MPQ implementation)
2398                let total_files = if let Some(ref block_table) = self.block_table {
2399                    block_table.entries().len()
2400                } else if let Some(ref bet_table) = self.bet_table {
2401                    bet_table.header.file_count as usize
2402                } else {
2403                    return Err(Error::invalid_format(
2404                        "No block/BET table available for attributes",
2405                    ));
2406                };
2407
2408                // Determine the actual block count by checking the attributes file structure
2409                // We'll try the full count first, then fall back to count-1 if that fails
2410                let block_count = {
2411                    // Calculate expected size with full file count
2412                    let flags_from_data = if data.len() >= 8 {
2413                        u32::from_le_bytes([data[4], data[5], data[6], data[7]])
2414                    } else {
2415                        0
2416                    };
2417
2418                    let mut expected_size_full = 8; // header
2419                    if flags_from_data & 0x01 != 0 {
2420                        expected_size_full += total_files * 4;
2421                    } // CRC32
2422                    if flags_from_data & 0x02 != 0 {
2423                        expected_size_full += total_files * 8;
2424                    } // FILETIME  
2425                    if flags_from_data & 0x04 != 0 {
2426                        expected_size_full += total_files * 16;
2427                    } // MD5
2428                    if flags_from_data & 0x08 != 0 {
2429                        expected_size_full += total_files.div_ceil(8);
2430                    } // PATCH_BIT
2431
2432                    if data.len() == expected_size_full {
2433                        // Perfect match with full file count - attributes includes itself
2434                        log::debug!(
2435                            "Attributes file contains entries for all {total_files} files (including itself)"
2436                        );
2437                        total_files
2438                    } else {
2439                        // Try with count-1 (traditional behavior)
2440                        let count_minus_1 = total_files.saturating_sub(1);
2441                        let mut expected_size_minus1 = 8; // header
2442                        if flags_from_data & 0x01 != 0 {
2443                            expected_size_minus1 += count_minus_1 * 4;
2444                        }
2445                        if flags_from_data & 0x02 != 0 {
2446                            expected_size_minus1 += count_minus_1 * 8;
2447                        }
2448                        if flags_from_data & 0x04 != 0 {
2449                            expected_size_minus1 += count_minus_1 * 16;
2450                        }
2451                        if flags_from_data & 0x08 != 0 {
2452                            expected_size_minus1 += count_minus_1.div_ceil(8);
2453                        }
2454
2455                        if data.len() == expected_size_minus1 {
2456                            log::debug!(
2457                                "Attributes file contains entries for {count_minus_1} files (excluding itself)"
2458                            );
2459                            count_minus_1
2460                        } else {
2461                            // Neither exact match - use full count and let the parser handle the discrepancy
2462                            log::debug!(
2463                                "Attributes file size doesn't match expected patterns, using full count {total_files} (actual: {}, expected_full: {expected_size_full}, expected_minus1: {expected_size_minus1})",
2464                                data.len()
2465                            );
2466                            total_files
2467                        }
2468                    }
2469                };
2470
2471                // Check if attributes data needs additional decompression
2472                // Some MPQ files have doubly-compressed attributes
2473                if data.len() >= 4 {
2474                    let first_dword = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
2475
2476                    // Check if this looks like compressed data instead of version 100
2477                    if first_dword != 100 && data[0] != 0x64 {
2478                        log::debug!(
2479                            "Attributes file may be compressed, first dword: 0x{:08X} ({}), first byte: 0x{:02X}",
2480                            first_dword,
2481                            first_dword,
2482                            data[0]
2483                        );
2484
2485                        // Try to decompress if it looks like compression flags
2486                        if data[0] & 0x0F != 0 || data[0] == 0x02 {
2487                            log::info!(
2488                                "Attempting to decompress attributes file with method 0x{:02X}",
2489                                data[0]
2490                            );
2491                            match compression::decompress(&data[1..], data[0], block_count * 100) {
2492                                Ok(decompressed) => {
2493                                    log::info!("Successfully decompressed attributes file");
2494                                    data = decompressed;
2495                                }
2496                                Err(e) => {
2497                                    log::warn!("Failed to decompress attributes file: {e}");
2498                                    // Continue with original data
2499                                }
2500                            }
2501                        }
2502                    }
2503                }
2504
2505                // Parse attributes
2506                let attributes = special_files::Attributes::parse(&data.into(), block_count)?;
2507                self.attributes = Some(attributes);
2508
2509                log::info!("Loaded (attributes) file with {block_count} entries");
2510                Ok(())
2511            }
2512            Err(Error::FileNotFound(_)) => {
2513                log::debug!("No (attributes) file found in archive");
2514                Ok(())
2515            }
2516            Err(e) => Err(e),
2517        }
2518    }
2519
2520    /// Get attributes for a specific file by block index
2521    pub fn get_file_attributes(
2522        &self,
2523        block_index: usize,
2524    ) -> Option<&special_files::FileAttributes> {
2525        self.attributes.as_ref()?.get_file_attributes(block_index)
2526    }
2527
2528    /// Get all loaded attributes
2529    pub fn attributes(&self) -> Option<&special_files::Attributes> {
2530        self.attributes.as_ref()
2531    }
2532
2533    /// Add a file to the archive
2534    pub fn add_file(&mut self, _name: &str, _data: &[u8]) -> Result<()> {
2535        Err(Error::invalid_format(
2536            "In-place file addition not yet implemented. Use ArchiveBuilder to create new archives.",
2537        ))
2538    }
2539
2540    /// Read HET table size from the table header for V3 archives
2541    fn read_het_table_size(&mut self, het_pos: u64) -> Result<u64> {
2542        // For compressed tables, calculate the actual size based on the next table position
2543        log::debug!("Determining HET table size from file structure");
2544
2545        // Calculate the actual size based on what comes after HET table
2546        let actual_size = if let Some(bet_pos) = self.header.bet_table_pos {
2547            if bet_pos > het_pos {
2548                // BET table comes after HET
2549                bet_pos - het_pos
2550            } else {
2551                // Calculate from hash table position
2552                self.header.get_hash_table_pos() - het_pos
2553            }
2554        } else {
2555            // Calculate from hash table position
2556            self.header.get_hash_table_pos() - het_pos
2557        };
2558
2559        log::debug!("HET table position: 0x{het_pos:X}, calculated size: {actual_size} bytes");
2560
2561        Ok(actual_size)
2562    }
2563
2564    /// Read BET table size from the table header for V3 archives
2565    fn read_bet_table_size(&mut self, bet_pos: u64) -> Result<u64> {
2566        // For compressed tables, calculate the actual size based on the next table position
2567        log::debug!("Determining BET table size from file structure");
2568
2569        // Calculate the actual size based on what comes after BET table (usually hash table)
2570        let actual_size = self.header.get_hash_table_pos() - bet_pos;
2571
2572        log::debug!("BET table position: 0x{bet_pos:X}, calculated size: {actual_size} bytes");
2573
2574        Ok(actual_size)
2575    }
2576
2577    /// Verify the digital signature of the archive
2578    pub fn verify_signature(&mut self) -> Result<SignatureStatus> {
2579        // First check for strong signature (external to archive)
2580        if let Ok(strong_status) = self.verify_strong_signature()
2581            && strong_status != SignatureStatus::None
2582        {
2583            return Ok(strong_status);
2584        }
2585
2586        // Then check for weak signature (inside archive)
2587        self.verify_weak_signature()
2588    }
2589
2590    /// Verify weak signature from (signature) file inside the archive
2591    fn verify_weak_signature(&mut self) -> Result<SignatureStatus> {
2592        // Check if (signature) file exists
2593        let signature_info = match self.find_file("(signature)")? {
2594            Some(info) => info,
2595            None => return Ok(SignatureStatus::None),
2596        };
2597
2598        // Read the signature file
2599        let signature_data = self.read_file("(signature)")?;
2600
2601        // Try to parse as weak signature
2602        match crate::crypto::parse_weak_signature(&signature_data) {
2603            Ok(weak_sig) => {
2604                // Create signature info for StormLib-compatible hash calculation
2605                let archive_size = self.header.archive_size as u64;
2606                let sig_info = crate::crypto::SignatureInfo::new_weak(
2607                    self.archive_offset,
2608                    archive_size,
2609                    signature_info.file_pos,
2610                    signature_info.compressed_size,
2611                    weak_sig.clone(),
2612                );
2613
2614                // Seek to beginning of archive
2615                self.reader.seek(SeekFrom::Start(self.archive_offset))?;
2616
2617                // Verify the weak signature using StormLib-compatible approach
2618                match crate::crypto::verify_weak_signature_stormlib(
2619                    &mut self.reader,
2620                    &weak_sig,
2621                    &sig_info,
2622                ) {
2623                    Ok(true) => Ok(SignatureStatus::WeakValid),
2624                    Ok(false) => Ok(SignatureStatus::WeakInvalid),
2625                    Err(e) => {
2626                        log::warn!("Failed to verify weak signature: {e}");
2627                        Ok(SignatureStatus::WeakInvalid)
2628                    }
2629                }
2630            }
2631            Err(_) => {
2632                // Not a weak signature
2633                log::debug!("Signature file found but not a valid weak signature format");
2634                Ok(SignatureStatus::None)
2635            }
2636        }
2637    }
2638
2639    /// Read a compressed and encrypted table from a V4 archive.
2640    ///
2641    /// V4 hash/block tables are encrypted on disk. The entire compressed blob
2642    /// (including the compression type byte) must be decrypted before the
2643    /// compression type can be read and decompression applied.
2644    /// This matches StormLib's `LoadMpqTable` behavior.
2645    fn read_compressed_encrypted_table(
2646        &mut self,
2647        offset: u64,
2648        compressed_size: u64,
2649        uncompressed_size: usize,
2650        key: u32,
2651    ) -> Result<Vec<u8>> {
2652        self.reader.seek(SeekFrom::Start(offset))?;
2653
2654        let mut raw_data = vec![0u8; compressed_size as usize];
2655        self.reader.read_exact(&mut raw_data)?;
2656
2657        if (compressed_size as usize) < uncompressed_size {
2658            // Decrypt the entire blob before reading the compression type
2659            let full_len = (raw_data.len() / 4) * 4;
2660            let mut u32_buffer: Vec<u32> = raw_data[..full_len]
2661                .chunks_exact(4)
2662                .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
2663                .collect();
2664
2665            decrypt_block(&mut u32_buffer, key);
2666
2667            for (i, &val) in u32_buffer.iter().enumerate() {
2668                let bytes = val.to_le_bytes();
2669                raw_data[i * 4..(i + 1) * 4].copy_from_slice(&bytes);
2670            }
2671
2672            // Now byte 0 is the (decrypted) compression type
2673            if raw_data.is_empty() {
2674                return Err(Error::invalid_format("Empty compressed table data"));
2675            }
2676
2677            let compression_type = raw_data[0];
2678            let compressed_content = &raw_data[1..];
2679
2680            log::debug!(
2681                "Decompressing encrypted table with method 0x{compression_type:02X}, \
2682                 compressed_size={compressed_size}, uncompressed_size={uncompressed_size}"
2683            );
2684
2685            compression::decompress(compressed_content, compression_type, uncompressed_size)
2686        } else {
2687            // Not compressed — just decrypt in place
2688            let full_len = (raw_data.len() / 4) * 4;
2689            let mut u32_buffer: Vec<u32> = raw_data[..full_len]
2690                .chunks_exact(4)
2691                .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
2692                .collect();
2693
2694            decrypt_block(&mut u32_buffer, key);
2695
2696            for (i, &val) in u32_buffer.iter().enumerate() {
2697                let bytes = val.to_le_bytes();
2698                raw_data[i * 4..(i + 1) * 4].copy_from_slice(&bytes);
2699            }
2700
2701            Ok(raw_data[..uncompressed_size].to_vec())
2702        }
2703    }
2704
2705    /// Read a potentially compressed table from the archive
2706    ///
2707    /// This handles V4 archives where hash/block tables can be compressed.
2708    /// The compressed data format is:
2709    /// - Compression type byte (e.g., 0x02 for ZLIB)
2710    /// - Compressed data
2711    fn read_compressed_table(
2712        &mut self,
2713        offset: u64,
2714        compressed_size: u64,
2715        uncompressed_size: usize,
2716    ) -> Result<Vec<u8>> {
2717        // Seek to the table position
2718        self.reader.seek(SeekFrom::Start(offset))?;
2719
2720        // Read the compressed data
2721        let mut compressed_data = vec![0u8; compressed_size as usize];
2722        self.reader.read_exact(&mut compressed_data)?;
2723
2724        // Check if the table is actually compressed
2725        // In V4 archives, if compressed_size < expected uncompressed size, it's compressed
2726        let expected_uncompressed_size = uncompressed_size;
2727
2728        if (compressed_size as usize) < expected_uncompressed_size {
2729            // Table is compressed
2730            log::debug!(
2731                "Table is compressed: compressed_size={compressed_size}, uncompressed_size={expected_uncompressed_size}"
2732            );
2733
2734            // First byte is the compression type
2735            if compressed_data.is_empty() {
2736                return Err(Error::invalid_format("Empty compressed table data"));
2737            }
2738
2739            let compression_type = compressed_data[0];
2740            let compressed_content = &compressed_data[1..];
2741
2742            log::debug!("Decompressing table with method 0x{compression_type:02X}");
2743
2744            // Decompress the data
2745            compression::decompress(
2746                compressed_content,
2747                compression_type,
2748                expected_uncompressed_size,
2749            )
2750        } else {
2751            // Table is not compressed, return as-is
2752            log::debug!("Table is not compressed, using as-is");
2753            Ok(compressed_data[..expected_uncompressed_size].to_vec())
2754        }
2755    }
2756
2757    /// Verify strong signature appended after the archive
2758    fn verify_strong_signature(&mut self) -> Result<SignatureStatus> {
2759        use crate::crypto::{
2760            STRONG_SIGNATURE_SIZE, parse_strong_signature, verify_strong_signature,
2761        };
2762
2763        // Get total file size
2764        let file_size = self.reader.get_ref().metadata()?.len();
2765
2766        // Calculate expected archive end position
2767        let archive_end = self.archive_offset + self.header.get_archive_size();
2768
2769        // Check if there's enough space for a strong signature after the archive
2770        if file_size < archive_end + STRONG_SIGNATURE_SIZE as u64 {
2771            log::debug!("File too small for strong signature");
2772            return Ok(SignatureStatus::None);
2773        }
2774
2775        // Seek to where the strong signature should be
2776        let signature_pos = archive_end;
2777        self.reader.seek(SeekFrom::Start(signature_pos))?;
2778
2779        // Read potential strong signature data
2780        let mut signature_data = vec![0u8; STRONG_SIGNATURE_SIZE];
2781        match self.reader.read_exact(&mut signature_data) {
2782            Ok(()) => {
2783                // Try to parse as strong signature
2784                match parse_strong_signature(&signature_data) {
2785                    Ok(strong_sig) => {
2786                        log::debug!("Found strong signature at offset 0x{signature_pos:X}");
2787
2788                        // Seek to beginning of archive for verification
2789                        self.reader.seek(SeekFrom::Start(self.archive_offset))?;
2790
2791                        // Verify the strong signature
2792                        match verify_strong_signature(
2793                            &mut self.reader,
2794                            &strong_sig,
2795                            archive_end - self.archive_offset,
2796                        ) {
2797                            Ok(true) => {
2798                                log::info!("Strong signature verification successful");
2799                                Ok(SignatureStatus::StrongValid)
2800                            }
2801                            Ok(false) => {
2802                                log::warn!("Strong signature verification failed");
2803                                Ok(SignatureStatus::StrongInvalid)
2804                            }
2805                            Err(e) => {
2806                                log::warn!("Failed to verify strong signature: {e}");
2807                                Ok(SignatureStatus::StrongInvalid)
2808                            }
2809                        }
2810                    }
2811                    Err(_) => {
2812                        // Not a strong signature
2813                        log::debug!("No valid strong signature found");
2814                        Ok(SignatureStatus::None)
2815                    }
2816                }
2817            }
2818            Err(e) => {
2819                log::debug!("Failed to read potential strong signature: {e}");
2820                Ok(SignatureStatus::None)
2821            }
2822        }
2823    }
2824}
2825
2826/// Decrypt file data in-place
2827pub fn decrypt_file_data(data: &mut [u8], key: u32) {
2828    if data.is_empty() || key == 0 {
2829        return;
2830    }
2831
2832    // Process full u32 chunks
2833    let chunks = data.len() / 4;
2834    if chunks > 0 {
2835        // Create a properly aligned u32 slice
2836        let mut u32_data = Vec::with_capacity(chunks);
2837
2838        // Copy data as u32 values (little-endian)
2839        for i in 0..chunks {
2840            let offset = i * 4;
2841            let value = u32::from_le_bytes([
2842                data[offset],
2843                data[offset + 1],
2844                data[offset + 2],
2845                data[offset + 3],
2846            ]);
2847            u32_data.push(value);
2848        }
2849
2850        // Decrypt the u32 data
2851        decrypt_block(&mut u32_data, key);
2852
2853        // Copy back to byte array
2854        for (i, &value) in u32_data.iter().enumerate() {
2855            let offset = i * 4;
2856            let bytes = value.to_le_bytes();
2857            data[offset] = bytes[0];
2858            data[offset + 1] = bytes[1];
2859            data[offset + 2] = bytes[2];
2860            data[offset + 3] = bytes[3];
2861        }
2862    }
2863
2864    // Handle remaining bytes if not aligned to 4
2865    let remainder = data.len() % 4;
2866    if remainder > 0 {
2867        let offset = chunks * 4;
2868
2869        // Read remaining bytes into a u32 (padding with zeros)
2870        let mut last_bytes = [0u8; 4];
2871        last_bytes[..remainder].copy_from_slice(&data[offset..(remainder + offset)]);
2872        let last_dword = u32::from_le_bytes(last_bytes);
2873
2874        // Decrypt with adjusted key
2875        let decrypted = decrypt_dword(last_dword, key.wrapping_add(chunks as u32));
2876
2877        // Write back only the remainder bytes
2878        let decrypted_bytes = decrypted.to_le_bytes();
2879        data[offset..(remainder + offset)].copy_from_slice(&decrypted_bytes[..remainder]);
2880    }
2881}
2882
2883/// Information about a file in the archive
2884#[derive(Debug)]
2885pub struct FileInfo {
2886    /// File name
2887    pub filename: String,
2888    /// Index in hash table
2889    pub hash_index: usize,
2890    /// Index in block table
2891    pub block_index: usize,
2892    /// Absolute file position in archive file
2893    pub file_pos: u64,
2894    /// Compressed size
2895    pub compressed_size: u64,
2896    /// Uncompressed size
2897    pub file_size: u64,
2898    /// File flags
2899    pub flags: u32,
2900    /// File locale
2901    pub locale: u16,
2902}
2903
2904impl FileInfo {
2905    /// Check if the file is compressed
2906    pub fn is_compressed(&self) -> bool {
2907        use crate::tables::BlockEntry;
2908        (self.flags & (BlockEntry::FLAG_IMPLODE | BlockEntry::FLAG_COMPRESS)) != 0
2909    }
2910
2911    /// Check if the file is encrypted
2912    pub fn is_encrypted(&self) -> bool {
2913        use crate::tables::BlockEntry;
2914        (self.flags & BlockEntry::FLAG_ENCRYPTED) != 0
2915    }
2916
2917    /// Check if the file has fixed key encryption
2918    pub fn has_fix_key(&self) -> bool {
2919        use crate::tables::BlockEntry;
2920        (self.flags & BlockEntry::FLAG_FIX_KEY) != 0
2921    }
2922
2923    /// Check if the file is stored as a single unit
2924    pub fn is_single_unit(&self) -> bool {
2925        use crate::tables::BlockEntry;
2926        (self.flags & BlockEntry::FLAG_SINGLE_UNIT) != 0
2927    }
2928
2929    /// Check if the file has sector CRCs
2930    pub fn has_sector_crc(&self) -> bool {
2931        use crate::tables::BlockEntry;
2932        (self.flags & BlockEntry::FLAG_SECTOR_CRC) != 0
2933    }
2934
2935    /// Check if the file is a patch file
2936    pub fn is_patch_file(&self) -> bool {
2937        use crate::tables::BlockEntry;
2938        (self.flags & BlockEntry::FLAG_PATCH_FILE) != 0
2939    }
2940
2941    /// Check if the file uses IMPLODE compression specifically
2942    pub fn is_implode(&self) -> bool {
2943        use crate::tables::BlockEntry;
2944        (self.flags & BlockEntry::FLAG_IMPLODE) != 0
2945            && (self.flags & BlockEntry::FLAG_COMPRESS) == 0
2946    }
2947
2948    /// Check if the file uses COMPRESS (multi-method compression)
2949    pub fn uses_compression_prefix(&self) -> bool {
2950        use crate::tables::BlockEntry;
2951        (self.flags & BlockEntry::FLAG_COMPRESS) != 0
2952    }
2953
2954    /// Extract compression method from block table flags
2955    /// Returns the compression method byte that should be used for decompression
2956    pub fn get_compression_method(&self) -> Option<u8> {
2957        use crate::compression::flags;
2958
2959        if !self.is_compressed() {
2960            return None;
2961        }
2962
2963        // Extract compression method from flags (MPQ_FILE_COMPRESS_MASK = 0x0000FF00)
2964        let compression_mask = (self.flags & 0x0000FF00) >> 8;
2965
2966        log::debug!(
2967            "Compression method extraction: flags=0x{:08X}, mask=0x{:02X}",
2968            self.flags,
2969            compression_mask
2970        );
2971
2972        // Convert from StormLib block table flag format to compression method byte format
2973        match compression_mask {
2974            0x02 => Some(flags::ZLIB),         // ZLIB/DEFLATE
2975            0x01 => Some(flags::IMPLODE),      // IMPLODE
2976            0x08 => Some(flags::PKWARE),       // PKWARE
2977            0x10 => Some(flags::BZIP2),        // BZIP2
2978            0x20 => Some(flags::SPARSE),       // SPARSE
2979            0x40 => Some(flags::ADPCM_MONO),   // ADPCM_MONO
2980            0x80 => Some(flags::ADPCM_STEREO), // ADPCM_STEREO
2981            _ => {
2982                log::warn!("Unknown compression method in flags: 0x{compression_mask:02X}");
2983                None
2984            }
2985        }
2986    }
2987}
2988
2989/// Information about a file in the archive (for listing)
2990#[derive(Debug)]
2991pub struct FileEntry {
2992    /// File name
2993    pub name: String,
2994    /// Uncompressed size
2995    pub size: u64,
2996    /// Compressed size
2997    pub compressed_size: u64,
2998    /// File flags
2999    pub flags: u32,
3000    /// Hash values (name_1, name_2) - only populated when requested
3001    pub hashes: Option<(u32, u32)>,
3002    /// Table indices for direct file access (when name is generic)
3003    /// Contains (hash_index, block_index) for classic tables or (file_index, None) for HET/BET
3004    pub table_indices: Option<(usize, Option<usize>)>,
3005}
3006
3007impl FileEntry {
3008    /// Check if the file is compressed
3009    pub fn is_compressed(&self) -> bool {
3010        use crate::tables::BlockEntry;
3011        (self.flags & (BlockEntry::FLAG_IMPLODE | BlockEntry::FLAG_COMPRESS)) != 0
3012    }
3013
3014    /// Check if the file is encrypted
3015    pub fn is_encrypted(&self) -> bool {
3016        use crate::tables::BlockEntry;
3017        (self.flags & BlockEntry::FLAG_ENCRYPTED) != 0
3018    }
3019
3020    /// Check if the file uses fixed key encryption
3021    pub fn has_fix_key(&self) -> bool {
3022        use crate::tables::BlockEntry;
3023        (self.flags & BlockEntry::FLAG_FIX_KEY) != 0
3024    }
3025
3026    /// Check if the file is stored as a single unit
3027    pub fn is_single_unit(&self) -> bool {
3028        use crate::tables::BlockEntry;
3029        (self.flags & BlockEntry::FLAG_SINGLE_UNIT) != 0
3030    }
3031
3032    /// Check if the file has sector CRCs
3033    pub fn has_sector_crc(&self) -> bool {
3034        use crate::tables::BlockEntry;
3035        (self.flags & BlockEntry::FLAG_SECTOR_CRC) != 0
3036    }
3037
3038    /// Check if the file exists
3039    pub fn exists(&self) -> bool {
3040        use crate::tables::BlockEntry;
3041        (self.flags & BlockEntry::FLAG_EXISTS) != 0
3042    }
3043
3044    /// Check if the file is a patch file (Cataclysm+ PTCH format)
3045    pub fn is_patch_file(&self) -> bool {
3046        use crate::tables::BlockEntry;
3047        (self.flags & BlockEntry::FLAG_PATCH_FILE) != 0
3048    }
3049}
3050
3051#[cfg(test)]
3052mod tests {
3053    use super::*;
3054    use crate::encrypt_block;
3055
3056    #[test]
3057    fn test_open_options() {
3058        let opts = OpenOptions::new().load_tables(false);
3059
3060        assert!(!opts.load_tables);
3061    }
3062
3063    #[test]
3064    fn test_file_info_flags() {
3065        use crate::tables::BlockEntry;
3066
3067        let info = FileInfo {
3068            filename: "test.txt".to_string(),
3069            hash_index: 0,
3070            block_index: 0,
3071            file_pos: 0,
3072            compressed_size: 100,
3073            file_size: 200,
3074            flags: BlockEntry::FLAG_COMPRESS | BlockEntry::FLAG_ENCRYPTED,
3075            locale: 0,
3076        };
3077
3078        assert!(info.is_compressed());
3079        assert!(info.is_encrypted());
3080        assert!(!info.has_fix_key());
3081    }
3082
3083    #[test]
3084    fn test_decrypt_file_data() {
3085        let mut data = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
3086        let original = data.clone();
3087
3088        // For testing, we need an encrypt function
3089        fn encrypt_test_data(data: &mut [u8], key: u32) {
3090            if data.is_empty() || key == 0 {
3091                return;
3092            }
3093
3094            // Convert to u32 for encryption
3095            let chunks = data.len() / 4;
3096            if chunks > 0 {
3097                let mut u32_data = Vec::with_capacity(chunks);
3098                for i in 0..chunks {
3099                    let offset = i * 4;
3100                    let value = u32::from_le_bytes([
3101                        data[offset],
3102                        data[offset + 1],
3103                        data[offset + 2],
3104                        data[offset + 3],
3105                    ]);
3106                    u32_data.push(value);
3107                }
3108
3109                encrypt_block(&mut u32_data, key);
3110
3111                for (i, &value) in u32_data.iter().enumerate() {
3112                    let offset = i * 4;
3113                    let bytes = value.to_le_bytes();
3114                    data[offset] = bytes[0];
3115                    data[offset + 1] = bytes[1];
3116                    data[offset + 2] = bytes[2];
3117                    data[offset + 3] = bytes[3];
3118                }
3119            }
3120        }
3121
3122        // Encrypt
3123        encrypt_test_data(&mut data, 0xDEADBEEF);
3124        assert_ne!(data, original, "Data should be changed after encryption");
3125
3126        // Decrypt
3127        decrypt_file_data(&mut data, 0xDEADBEEF);
3128        assert_eq!(data, original, "Data should be restored after decryption");
3129    }
3130
3131    #[test]
3132    fn test_crc_calculation() {
3133        // Test that we're using the correct checksum algorithm (ADLER32)
3134        // MPQ uses ADLER32 for sector checksums, not CRC32 despite the name "SECTOR_CRC"
3135        let test_data = b"Hello, World!";
3136        let crc = adler2::adler32_slice(test_data);
3137
3138        // This is the expected ADLER32 value for "Hello, World!"
3139        assert_eq!(crc, 0x1F9E046A);
3140    }
3141}