1use 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#[derive(Debug, Clone)]
27pub struct ArchiveInfo {
28 pub path: PathBuf,
30 pub file_size: u64,
32 pub archive_offset: u64,
34 pub format_version: header::FormatVersion,
36 pub file_count: usize,
38 pub max_file_count: u32,
40 pub sector_size: usize,
42 pub is_encrypted: bool,
44 pub has_signature: bool,
46 pub signature_status: SignatureStatus,
48 pub hash_table_info: TableInfo,
50 pub block_table_info: TableInfo,
52 pub het_table_info: Option<TableInfo>,
54 pub bet_table_info: Option<TableInfo>,
56 pub hi_block_table_info: Option<TableInfo>,
58 pub has_attributes: bool,
60 pub has_listfile: bool,
62 pub user_data_info: Option<UserDataInfo>,
64 pub md5_status: Option<Md5Status>,
66}
67
68#[derive(Debug, Clone)]
70pub struct TableInfo {
71 pub size: Option<u32>,
73 pub offset: u64,
75 pub compressed_size: Option<u64>,
77 pub failed_to_load: bool,
79}
80
81#[derive(Debug, Clone)]
83pub struct UserDataInfo {
84 pub header_size: u32,
86 pub data_size: u32,
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub enum SignatureStatus {
93 None,
95 WeakValid,
97 WeakInvalid,
99 StrongValid,
101 StrongInvalid,
103 StrongNoKey,
105}
106
107#[derive(Debug, Clone)]
109pub struct Md5Status {
110 pub hash_table_valid: bool,
112 pub block_table_valid: bool,
114 pub hi_block_table_valid: bool,
116 pub het_table_valid: bool,
118 pub bet_table_valid: bool,
120 pub header_valid: bool,
122}
123
124#[derive(Debug, Clone)]
144pub struct OpenOptions {
145 pub load_tables: bool,
154
155 version: Option<crate::header::FormatVersion>,
160}
161
162impl OpenOptions {
163 pub fn new() -> Self {
169 Self {
170 load_tables: true,
171 version: None,
172 }
173 }
174
175 pub fn load_tables(mut self, load: bool) -> Self {
184 self.load_tables = load;
185 self
186 }
187
188 pub fn version(mut self, version: crate::header::FormatVersion) -> Self {
199 self.version = Some(version);
200 self
201 }
202
203 pub fn open<P: AsRef<Path>>(self, path: P) -> Result<Archive> {
216 Archive::open_with_options(path, self)
217 }
218
219 pub fn create<P: AsRef<Path>>(self, path: P) -> Result<Archive> {
234 let path = path.as_ref();
235
236 let builder =
238 ArchiveBuilder::new().version(self.version.unwrap_or(crate::header::FormatVersion::V1));
239
240 builder.build(path)?;
242
243 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#[derive(Debug)]
256pub struct Archive {
257 path: PathBuf,
259 reader: BufReader<File>,
261 archive_offset: u64,
263 user_data: Option<UserDataHeader>,
265 header: MpqHeader,
267 hash_table: Option<HashTable>,
269 block_table: Option<BlockTable>,
271 hi_block_table: Option<HiBlockTable>,
273 het_table: Option<HetTable>,
275 bet_table: Option<BetTable>,
277 attributes: Option<special_files::Attributes>,
279}
280
281impl Archive {
282 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
284 Self::open_with_options(path, OpenOptions::default())
285 }
286
287 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 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 if options.load_tables {
312 archive.load_tables()?;
313 }
314
315 Ok(archive)
316 }
317
318 pub fn load_tables(&mut self) -> Result<()> {
320 log::debug!(
321 "Loading tables for archive version {:?}",
322 self.header.format_version
323 );
324
325 if self.header.format_version >= header::FormatVersion::V3 {
327 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 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 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 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 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 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 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 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 self.reader
423 .seek(SeekFrom::Start(self.archive_offset + bet_pos))?;
424
425 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 let _has_valid_het_bet = match (&self.het_table, &self.bet_table) {
450 (Some(het), Some(bet)) => {
451 het.header.max_file_count > 0 && bet.header.file_count > 0
453 }
454 _ => false,
455 };
456
457 if self.header.hash_table_size > 0 {
460 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; if let Some(v4_data) = &self.header.v4_data {
466 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); if v4_size_valid {
473 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 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 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 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 log::warn!(
516 "V4 archive has invalid compressed size ({}), using heuristic detection",
517 v4_data.hash_table_size_64
518 );
519 }
521 }
522
523 if self.hash_table.is_none() {
525 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 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 log::debug!(
539 "V3 hash table appears compressed: available space {available_space} < expected size {uncompressed_size}"
540 );
541
542 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 let entries_that_fit = available_space / 16; 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 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 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; if let Some(v4_data) = &self.header.v4_data {
615 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); if v4_size_valid {
622 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 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 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 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 log::warn!(
665 "V4 archive has invalid compressed size ({}), using heuristic detection",
666 v4_data.block_table_size_64
667 );
668 }
670 }
671
672 if self.block_table.is_none() {
674 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 log::debug!(
692 "V3 block table appears compressed: available space {available_space} < expected size {uncompressed_size}"
693 );
694
695 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 let entries_that_fit = available_space / 16; 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 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 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 match self.load_attributes() {
780 Ok(()) => {}
781 Err(e) => {
782 log::warn!("Failed to load attributes: {e:?}");
783 }
785 }
786
787 Ok(())
788 }
789
790 pub fn header(&self) -> &MpqHeader {
792 &self.header
793 }
794
795 pub fn user_data(&self) -> Option<&UserDataHeader> {
797 self.user_data.as_ref()
798 }
799
800 pub fn archive_offset(&self) -> u64 {
802 self.archive_offset
803 }
804
805 pub fn path(&self) -> &Path {
807 &self.path
808 }
809
810 pub fn hi_block_table(&self) -> Option<&HiBlockTable> {
812 self.hi_block_table.as_ref()
813 }
814
815 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 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); }
832
833 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 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 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 };
866
867 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 };
875
876 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 };
887
888 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 };
899
900 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 };
911
912 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 pub fn get_info(&mut self) -> Result<ArchiveInfo> {
943 log::debug!("Getting archive info");
944
945 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 log::debug!("Getting file size");
953 let file_size = self.reader.get_ref().metadata()?.len();
954
955 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 block_table
961 .entries()
962 .iter()
963 .filter(|entry| entry.file_size != 0)
964 .count()
965 } else {
966 0
967 };
968
969 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 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 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 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 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 let mut compressed_size = self.header.v4_data.as_ref().map(|v4| v4.het_table_size_64);
1031
1032 if compressed_size.is_none() && self.header.format_version == header::FormatVersion::V3
1034 {
1035 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 let mut compressed_size = self.header.v4_data.as_ref().map(|v4| v4.bet_table_size_64);
1074
1075 if compressed_size.is_none() && self.header.format_version == header::FormatVersion::V3
1077 {
1078 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 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 pub fn hash_table(&self) -> Option<&HashTable> {
1168 self.hash_table.as_ref()
1169 }
1170
1171 pub fn block_table(&self) -> Option<&BlockTable> {
1173 self.block_table.as_ref()
1174 }
1175
1176 pub fn het_table(&self) -> Option<&HetTable> {
1178 self.het_table.as_ref()
1179 }
1180
1181 pub fn bet_table(&self) -> Option<&BetTable> {
1183 self.bet_table.as_ref()
1184 }
1185
1186 pub fn find_file(&self, filename: &str) -> Result<Option<FileInfo>> {
1188 let is_special_file = matches!(
1190 filename,
1191 "(listfile)" | "(attributes)" | "(signature)" | "(patch_metadata)"
1192 );
1193
1194 if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table) {
1196 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 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 for &candidate_index in &collision_candidates {
1214 if bet.verify_file_hash(candidate_index, filename) {
1216 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, 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, }));
1233 }
1234 }
1235 }
1236
1237 log::debug!(
1239 "HET/BET: '{}' not found - {} candidates checked, none matched BET hash",
1240 filename,
1241 collision_candidates.len()
1242 );
1243 }
1244
1245 if !is_special_file && (self.hash_table.is_none() || self.block_table.is_none()) {
1248 return Ok(None);
1249 }
1250 }
1251 }
1252
1253 self.find_file_classic(filename)
1259 }
1260
1261 fn find_file_classic(&self, filename: &str) -> Result<Option<FileInfo>> {
1263 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 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 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 pub fn list(&mut self) -> Result<Vec<FileEntry>> {
1305 if let Some(_listfile_info) = self.find_file("(listfile)")? {
1307 match self.read_file("(listfile)") {
1309 Ok(listfile_data) => {
1310 match special_files::parse_listfile(&listfile_data) {
1312 Ok(filenames) => {
1313 let mut entries = Vec::new();
1314
1315 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 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 log::info!("Enumerating anonymous entries");
1356
1357 let mut entries = Vec::new();
1358
1359 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 for i in 0..bet.header.file_count {
1368 if let Some(bet_info) = bet.get_file_info(i) {
1369 if bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0 {
1371 entries.push(FileEntry {
1372 name: format!("file_{i:08}.dat"), 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)), });
1379 }
1380 }
1381 }
1382
1383 if !entries.is_empty() {
1385 return Ok(entries);
1386 }
1387 }
1388
1389 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 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"), 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))), });
1415 }
1416 }
1417
1418 Ok(entries)
1419 }
1420
1421 pub fn list_all(&mut self) -> Result<Vec<FileEntry>> {
1424 let mut entries = Vec::new();
1425
1426 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 for i in 0..bet.header.file_count {
1435 if let Some(bet_info) = bet.get_file_info(i) {
1436 if bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0 {
1438 entries.push(FileEntry {
1439 name: format!("file_{i:08}.dat"), 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)), });
1446 }
1447 }
1448 }
1449
1450 if !entries.is_empty() {
1452 return Ok(entries);
1453 }
1454 }
1455
1456 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 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 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))), });
1491 }
1492 }
1493 }
1494
1495 entries.sort_by(|a, b| a.name.cmp(&b.name));
1497
1498 Ok(entries)
1499 }
1500
1501 pub fn list_with_hashes(&mut self) -> Result<Vec<FileEntry>> {
1503 let mut entries = self.list()?;
1504
1505 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 pub fn list_all_with_hashes(&mut self) -> Result<Vec<FileEntry>> {
1517 let mut entries = Vec::new();
1518
1519 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 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, table_indices: Some((i as usize, None)), });
1539 }
1540 }
1541
1542 if !entries.is_empty() {
1543 return Ok(entries);
1544 }
1545 }
1546
1547 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 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))), });
1581 }
1582 }
1583 }
1584
1585 entries.sort_by(|a, b| a.name.cmp(&b.name));
1587
1588 Ok(entries)
1589 }
1590
1591 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 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 let (file_size_for_key, actual_file_size) =
1610 if self.het_table.is_some() && self.bet_table.is_some() {
1611 (file_info.file_size as u32, file_info.file_size)
1613 } else {
1614 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 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 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 self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
1641
1642 if file_info.is_single_unit() || !file_info.is_compressed() {
1643 let mut data = vec![0u8; file_info.compressed_size as usize];
1645 self.reader.read_exact(&mut data)?;
1646
1647 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 if file_info.has_sector_crc() && file_info.is_single_unit() {
1665 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 let data_to_check = if file_info.is_compressed() {
1672 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 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 if file_info.is_compressed() {
1699 if file_info.is_single_unit() {
1700 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 Ok(data)
1714 } else if let Some(compression_method) = file_info.get_compression_method() {
1715 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 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 log::warn!("Non-single-unit compressed file in single-unit code path");
1748 Ok(data)
1749 }
1750 } else {
1751 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 self.read_sectored_file(&file_info, key)
1760 }
1761 }
1762
1763 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 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 let (file_size_for_key, _actual_file_size) =
1785 if self.het_table.is_some() && self.bet_table.is_some() {
1786 (file_info.file_size as u32, file_info.file_size)
1788 } else {
1789 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 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 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 self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
1817
1818 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 let actual_patch_size = patch_data_size as usize;
1850
1851 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 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 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 log::debug!("Patch file is sectored, reading with modified sector handling");
1898
1899 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 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 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: {:?}", §or_offsets);
1929
1930 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 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 §or_data[..16.min(sector_data.len())]
1959 );
1960
1961 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 let expected_size =
1973 sector_size.min(patch_data_size as usize - decompressed_data.len());
1974 let sector_decompressed = compression::decompress(
1975 §or_data[1..], 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(§or_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 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 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 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 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 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, block_index: 0, file_pos,
2061 compressed_size: bet_info.compressed_size,
2062 file_size: bet_info.file_size,
2063 flags: bet_info.flags,
2064 locale: 0, }
2066 };
2067
2068 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 let key = if file_info.is_encrypted() {
2083 hash_string(&file_info.filename, hash_type::FILE_KEY)
2085 } else {
2086 0
2087 };
2088
2089 let (file_size_for_key, actual_file_size) =
2091 if self.het_table.is_some() && self.bet_table.is_some() {
2092 (file_info.file_size as u32, file_info.file_size)
2094 } else {
2095 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 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 self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
2115
2116 if file_info.is_single_unit() || !file_info.is_compressed() {
2117 let mut data = vec![0u8; file_info.compressed_size as usize];
2119 self.reader.read_exact(&mut data)?;
2120
2121 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 if file_info.is_compressed() {
2133 if data.is_empty() {
2134 return Err(Error::compression("File data is empty"));
2135 }
2136
2137 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 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 self.read_sectored_file(&file_info, key)
2170 }
2171 }
2172
2173 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 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 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 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 let mut sector_crcs = None;
2226 if file_info.has_sector_crc() {
2227 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 let mut crc_data = vec![0u8; expected_crc_table_size];
2236 self.reader.read_exact(&mut crc_data)?;
2237
2238 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::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 let mut decompressed_data = Vec::with_capacity(file_info.file_size as usize);
2271
2272 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 log::warn!(
2285 "Invalid sector offsets detected: start={sector_start}, end={sector_end} for sector {i}. Attempting recovery."
2286 );
2287
2288 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 let remaining = file_info.file_size as usize - decompressed_data.len();
2299 let expected_size = remaining.min(sector_size);
2300
2301 self.reader
2303 .seek(SeekFrom::Start(file_info.file_pos + sector_start))?;
2304
2305 if sector_size_compressed > sector_buffer.len() {
2307 sector_buffer.resize(sector_size_compressed, 0);
2308 }
2309
2310 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 §or_data[..16.min(sector_data.len())]
2320 );
2321 }
2322
2323 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 if let Some(ref _crcs) = sector_crcs {
2332 log::trace!("Skipping CRC validation for sector {i}");
2335 }
2336
2337 let decompressed_sector = if file_info.is_compressed()
2339 && sector_size_compressed < expected_size
2340 {
2341 if !sector_data.is_empty() {
2342 if file_info.is_implode() {
2344 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 let compression_type = sector_data[0];
2357 let compressed_data = §or_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_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 pub fn load_attributes(&mut self) -> Result<()> {
2387 if self.attributes.is_some() {
2389 return Ok(());
2390 }
2391
2392 match self.read_file("(attributes)") {
2394 Ok(mut data) => {
2395 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 let block_count = {
2411 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; if flags_from_data & 0x01 != 0 {
2420 expected_size_full += total_files * 4;
2421 } if flags_from_data & 0x02 != 0 {
2423 expected_size_full += total_files * 8;
2424 } if flags_from_data & 0x04 != 0 {
2426 expected_size_full += total_files * 16;
2427 } if flags_from_data & 0x08 != 0 {
2429 expected_size_full += total_files.div_ceil(8);
2430 } if data.len() == expected_size_full {
2433 log::debug!(
2435 "Attributes file contains entries for all {total_files} files (including itself)"
2436 );
2437 total_files
2438 } else {
2439 let count_minus_1 = total_files.saturating_sub(1);
2441 let mut expected_size_minus1 = 8; 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 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 if data.len() >= 4 {
2474 let first_dword = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
2475
2476 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 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 }
2500 }
2501 }
2502 }
2503 }
2504
2505 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 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 pub fn attributes(&self) -> Option<&special_files::Attributes> {
2530 self.attributes.as_ref()
2531 }
2532
2533 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 fn read_het_table_size(&mut self, het_pos: u64) -> Result<u64> {
2542 log::debug!("Determining HET table size from file structure");
2544
2545 let actual_size = if let Some(bet_pos) = self.header.bet_table_pos {
2547 if bet_pos > het_pos {
2548 bet_pos - het_pos
2550 } else {
2551 self.header.get_hash_table_pos() - het_pos
2553 }
2554 } else {
2555 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 fn read_bet_table_size(&mut self, bet_pos: u64) -> Result<u64> {
2566 log::debug!("Determining BET table size from file structure");
2568
2569 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 pub fn verify_signature(&mut self) -> Result<SignatureStatus> {
2579 if let Ok(strong_status) = self.verify_strong_signature()
2581 && strong_status != SignatureStatus::None
2582 {
2583 return Ok(strong_status);
2584 }
2585
2586 self.verify_weak_signature()
2588 }
2589
2590 fn verify_weak_signature(&mut self) -> Result<SignatureStatus> {
2592 let signature_info = match self.find_file("(signature)")? {
2594 Some(info) => info,
2595 None => return Ok(SignatureStatus::None),
2596 };
2597
2598 let signature_data = self.read_file("(signature)")?;
2600
2601 match crate::crypto::parse_weak_signature(&signature_data) {
2603 Ok(weak_sig) => {
2604 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 self.reader.seek(SeekFrom::Start(self.archive_offset))?;
2616
2617 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 log::debug!("Signature file found but not a valid weak signature format");
2634 Ok(SignatureStatus::None)
2635 }
2636 }
2637 }
2638
2639 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 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 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 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 fn read_compressed_table(
2712 &mut self,
2713 offset: u64,
2714 compressed_size: u64,
2715 uncompressed_size: usize,
2716 ) -> Result<Vec<u8>> {
2717 self.reader.seek(SeekFrom::Start(offset))?;
2719
2720 let mut compressed_data = vec![0u8; compressed_size as usize];
2722 self.reader.read_exact(&mut compressed_data)?;
2723
2724 let expected_uncompressed_size = uncompressed_size;
2727
2728 if (compressed_size as usize) < expected_uncompressed_size {
2729 log::debug!(
2731 "Table is compressed: compressed_size={compressed_size}, uncompressed_size={expected_uncompressed_size}"
2732 );
2733
2734 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 compression::decompress(
2746 compressed_content,
2747 compression_type,
2748 expected_uncompressed_size,
2749 )
2750 } else {
2751 log::debug!("Table is not compressed, using as-is");
2753 Ok(compressed_data[..expected_uncompressed_size].to_vec())
2754 }
2755 }
2756
2757 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 let file_size = self.reader.get_ref().metadata()?.len();
2765
2766 let archive_end = self.archive_offset + self.header.get_archive_size();
2768
2769 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 let signature_pos = archive_end;
2777 self.reader.seek(SeekFrom::Start(signature_pos))?;
2778
2779 let mut signature_data = vec![0u8; STRONG_SIGNATURE_SIZE];
2781 match self.reader.read_exact(&mut signature_data) {
2782 Ok(()) => {
2783 match parse_strong_signature(&signature_data) {
2785 Ok(strong_sig) => {
2786 log::debug!("Found strong signature at offset 0x{signature_pos:X}");
2787
2788 self.reader.seek(SeekFrom::Start(self.archive_offset))?;
2790
2791 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 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
2826pub fn decrypt_file_data(data: &mut [u8], key: u32) {
2828 if data.is_empty() || key == 0 {
2829 return;
2830 }
2831
2832 let chunks = data.len() / 4;
2834 if chunks > 0 {
2835 let mut u32_data = Vec::with_capacity(chunks);
2837
2838 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_block(&mut u32_data, key);
2852
2853 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 let remainder = data.len() % 4;
2866 if remainder > 0 {
2867 let offset = chunks * 4;
2868
2869 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 let decrypted = decrypt_dword(last_dword, key.wrapping_add(chunks as u32));
2876
2877 let decrypted_bytes = decrypted.to_le_bytes();
2879 data[offset..(remainder + offset)].copy_from_slice(&decrypted_bytes[..remainder]);
2880 }
2881}
2882
2883#[derive(Debug)]
2885pub struct FileInfo {
2886 pub filename: String,
2888 pub hash_index: usize,
2890 pub block_index: usize,
2892 pub file_pos: u64,
2894 pub compressed_size: u64,
2896 pub file_size: u64,
2898 pub flags: u32,
2900 pub locale: u16,
2902}
2903
2904impl FileInfo {
2905 pub fn is_compressed(&self) -> bool {
2907 use crate::tables::BlockEntry;
2908 (self.flags & (BlockEntry::FLAG_IMPLODE | BlockEntry::FLAG_COMPRESS)) != 0
2909 }
2910
2911 pub fn is_encrypted(&self) -> bool {
2913 use crate::tables::BlockEntry;
2914 (self.flags & BlockEntry::FLAG_ENCRYPTED) != 0
2915 }
2916
2917 pub fn has_fix_key(&self) -> bool {
2919 use crate::tables::BlockEntry;
2920 (self.flags & BlockEntry::FLAG_FIX_KEY) != 0
2921 }
2922
2923 pub fn is_single_unit(&self) -> bool {
2925 use crate::tables::BlockEntry;
2926 (self.flags & BlockEntry::FLAG_SINGLE_UNIT) != 0
2927 }
2928
2929 pub fn has_sector_crc(&self) -> bool {
2931 use crate::tables::BlockEntry;
2932 (self.flags & BlockEntry::FLAG_SECTOR_CRC) != 0
2933 }
2934
2935 pub fn is_patch_file(&self) -> bool {
2937 use crate::tables::BlockEntry;
2938 (self.flags & BlockEntry::FLAG_PATCH_FILE) != 0
2939 }
2940
2941 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 pub fn uses_compression_prefix(&self) -> bool {
2950 use crate::tables::BlockEntry;
2951 (self.flags & BlockEntry::FLAG_COMPRESS) != 0
2952 }
2953
2954 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 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 match compression_mask {
2974 0x02 => Some(flags::ZLIB), 0x01 => Some(flags::IMPLODE), 0x08 => Some(flags::PKWARE), 0x10 => Some(flags::BZIP2), 0x20 => Some(flags::SPARSE), 0x40 => Some(flags::ADPCM_MONO), 0x80 => Some(flags::ADPCM_STEREO), _ => {
2982 log::warn!("Unknown compression method in flags: 0x{compression_mask:02X}");
2983 None
2984 }
2985 }
2986 }
2987}
2988
2989#[derive(Debug)]
2991pub struct FileEntry {
2992 pub name: String,
2994 pub size: u64,
2996 pub compressed_size: u64,
2998 pub flags: u32,
3000 pub hashes: Option<(u32, u32)>,
3002 pub table_indices: Option<(usize, Option<usize>)>,
3005}
3006
3007impl FileEntry {
3008 pub fn is_compressed(&self) -> bool {
3010 use crate::tables::BlockEntry;
3011 (self.flags & (BlockEntry::FLAG_IMPLODE | BlockEntry::FLAG_COMPRESS)) != 0
3012 }
3013
3014 pub fn is_encrypted(&self) -> bool {
3016 use crate::tables::BlockEntry;
3017 (self.flags & BlockEntry::FLAG_ENCRYPTED) != 0
3018 }
3019
3020 pub fn has_fix_key(&self) -> bool {
3022 use crate::tables::BlockEntry;
3023 (self.flags & BlockEntry::FLAG_FIX_KEY) != 0
3024 }
3025
3026 pub fn is_single_unit(&self) -> bool {
3028 use crate::tables::BlockEntry;
3029 (self.flags & BlockEntry::FLAG_SINGLE_UNIT) != 0
3030 }
3031
3032 pub fn has_sector_crc(&self) -> bool {
3034 use crate::tables::BlockEntry;
3035 (self.flags & BlockEntry::FLAG_SECTOR_CRC) != 0
3036 }
3037
3038 pub fn exists(&self) -> bool {
3040 use crate::tables::BlockEntry;
3041 (self.flags & BlockEntry::FLAG_EXISTS) != 0
3042 }
3043
3044 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 fn encrypt_test_data(data: &mut [u8], key: u32) {
3090 if data.is_empty() || key == 0 {
3091 return;
3092 }
3093
3094 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_test_data(&mut data, 0xDEADBEEF);
3124 assert_ne!(data, original, "Data should be changed after encryption");
3125
3126 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 let test_data = b"Hello, World!";
3136 let crc = adler2::adler32_slice(test_data);
3137
3138 assert_eq!(crc, 0x1F9E046A);
3140 }
3141}