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 match self.read_compressed_table(
487 hash_table_offset,
488 compressed_size,
489 uncompressed_size,
490 ) {
491 Ok(table_data) => {
492 match HashTable::from_bytes(
494 &table_data,
495 self.header.hash_table_size,
496 ) {
497 Ok(hash_table) => {
498 self.hash_table = Some(hash_table);
499 }
500 Err(e) => {
501 log::warn!("Failed to parse hash table: {e}");
502 }
503 }
504 }
505 Err(e) => {
506 log::warn!("Failed to decompress hash table: {e}");
507 }
508 }
509 }
510 } else {
511 log::warn!(
513 "V4 archive has invalid compressed size ({}), using heuristic detection",
514 v4_data.hash_table_size_64
515 );
516 }
518 }
519
520 if self.hash_table.is_none() {
522 let block_table_offset = self.archive_offset + self.header.get_block_table_pos();
525 let available_space = if block_table_offset > hash_table_offset {
526 (block_table_offset - hash_table_offset) as usize
527 } else {
528 let file_size = self.reader.get_ref().metadata()?.len();
530 (file_size - hash_table_offset) as usize
531 };
532
533 if available_space < uncompressed_size {
534 log::debug!(
536 "V3 hash table appears compressed: available space {available_space} < expected size {uncompressed_size}"
537 );
538
539 match self.read_compressed_table(
541 hash_table_offset,
542 available_space as u64,
543 uncompressed_size,
544 ) {
545 Ok(table_data) => {
546 match HashTable::from_bytes(&table_data, self.header.hash_table_size) {
547 Ok(hash_table) => {
548 self.hash_table = Some(hash_table);
549 }
550 Err(e) => {
551 log::warn!("Failed to parse hash table: {e}");
552 }
553 }
554 }
555 Err(e) => {
556 log::warn!("Failed to decompress hash table: {e}");
557 let entries_that_fit = available_space / 16; let mut pow2_entries = 1u32;
562 while pow2_entries * 2 <= entries_that_fit as u32 {
563 pow2_entries *= 2;
564 }
565 if pow2_entries > 0 {
566 log::warn!(
567 "Trying to read truncated hash table with {} entries (originally {})",
568 pow2_entries,
569 self.header.hash_table_size
570 );
571 match HashTable::read(
572 &mut self.reader,
573 hash_table_offset,
574 pow2_entries,
575 ) {
576 Ok(hash_table) => {
577 self.hash_table = Some(hash_table);
578 log::info!("Successfully loaded truncated hash table");
579 }
580 Err(e2) => {
581 log::warn!("Failed to read truncated hash table: {e2}");
582 }
583 }
584 }
585 }
586 }
587 } else {
588 match HashTable::read(
590 &mut self.reader,
591 hash_table_offset,
592 self.header.hash_table_size,
593 ) {
594 Ok(hash_table) => {
595 self.hash_table = Some(hash_table);
596 }
597 Err(e) => {
598 log::warn!("Failed to read hash table: {e}");
599 }
600 }
601 }
602 }
603 }
604
605 if self.header.block_table_size > 0 {
606 let block_table_offset = self.archive_offset + self.header.get_block_table_pos();
608 let uncompressed_size = self.header.block_table_size as usize * 16; if let Some(v4_data) = &self.header.v4_data {
612 let file_size = self.reader.get_ref().metadata()?.len();
614 let v4_size_valid = v4_data.block_table_size_64 > 0
615 && v4_data.block_table_size_64 < file_size
616 && v4_data.block_table_size_64 < (uncompressed_size as u64 * 2); if v4_size_valid {
619 let compressed_size = v4_data.block_table_size_64;
621
622 log::debug!(
623 "Loading block table from 0x{block_table_offset:X}, compressed size: {compressed_size} bytes, uncompressed size: {uncompressed_size} bytes"
624 );
625
626 let file_size = self.reader.get_ref().metadata()?.len();
628 if block_table_offset + compressed_size > file_size {
629 log::warn!("Block table extends beyond file, skipping");
630 } else {
631 match self.read_compressed_table(
633 block_table_offset,
634 compressed_size,
635 uncompressed_size,
636 ) {
637 Ok(table_data) => {
638 match BlockTable::from_bytes(
640 &table_data,
641 self.header.block_table_size,
642 ) {
643 Ok(block_table) => {
644 self.block_table = Some(block_table);
645 }
646 Err(e) => {
647 log::warn!("Failed to parse block table: {e}");
648 }
649 }
650 }
651 Err(e) => {
652 log::warn!("Failed to decompress block table: {e}");
653 }
654 }
655 }
656 } else {
657 log::warn!(
659 "V4 archive has invalid compressed size ({}), using heuristic detection",
660 v4_data.block_table_size_64
661 );
662 }
664 }
665
666 if self.block_table.is_none() {
668 let file_size = self.reader.get_ref().metadata()?.len();
671 let next_section = if let Some(hi_block_pos) = self.header.hi_block_table_pos {
672 if hi_block_pos != 0 {
673 self.archive_offset + hi_block_pos
674 } else {
675 file_size
676 }
677 } else {
678 file_size
679 };
680
681 let available_space = (next_section.saturating_sub(block_table_offset)) as usize;
682
683 if available_space < uncompressed_size {
684 log::debug!(
686 "V3 block table appears compressed: available space {available_space} < expected size {uncompressed_size}"
687 );
688
689 match self.read_compressed_table(
691 block_table_offset,
692 available_space as u64,
693 uncompressed_size,
694 ) {
695 Ok(table_data) => {
696 match BlockTable::from_bytes(&table_data, self.header.block_table_size)
697 {
698 Ok(block_table) => {
699 self.block_table = Some(block_table);
700 }
701 Err(e) => {
702 log::warn!("Failed to parse block table: {e}");
703 }
704 }
705 }
706 Err(e) => {
707 log::warn!("Failed to decompress block table: {e}");
708 let entries_that_fit = available_space / 16; if entries_that_fit > 0 {
712 log::warn!(
713 "Trying to read truncated block table with {} entries (originally {})",
714 entries_that_fit,
715 self.header.block_table_size
716 );
717 match BlockTable::read(
718 &mut self.reader,
719 block_table_offset,
720 entries_that_fit as u32,
721 ) {
722 Ok(block_table) => {
723 self.block_table = Some(block_table);
724 log::info!("Successfully loaded truncated block table");
725 }
726 Err(e2) => {
727 log::warn!("Failed to read truncated block table: {e2}");
728 }
729 }
730 }
731 }
732 }
733 } else {
734 match BlockTable::read(
736 &mut self.reader,
737 block_table_offset,
738 self.header.block_table_size,
739 ) {
740 Ok(block_table) => {
741 self.block_table = Some(block_table);
742 }
743 Err(e) => {
744 log::warn!("Failed to read block table: {e}");
745 }
746 }
747 }
748 }
749 }
750
751 if let Some(hi_block_pos) = self.header.hi_block_table_pos
753 && hi_block_pos != 0
754 {
755 let hi_block_offset = self.archive_offset + hi_block_pos;
756 let hi_block_end = hi_block_offset + (self.header.block_table_size as u64 * 8);
757
758 let file_size = self.reader.get_ref().metadata()?.len();
759 if hi_block_end > file_size {
760 log::warn!(
761 "Hi-block table extends beyond file (ends at 0x{hi_block_end:X}, file size 0x{file_size:X}). Skipping."
762 );
763 } else {
764 self.hi_block_table = Some(HiBlockTable::read(
765 &mut self.reader,
766 hi_block_offset,
767 self.header.block_table_size,
768 )?);
769 }
770 }
771
772 match self.load_attributes() {
774 Ok(()) => {}
775 Err(e) => {
776 log::warn!("Failed to load attributes: {e:?}");
777 }
779 }
780
781 Ok(())
782 }
783
784 pub fn header(&self) -> &MpqHeader {
786 &self.header
787 }
788
789 pub fn user_data(&self) -> Option<&UserDataHeader> {
791 self.user_data.as_ref()
792 }
793
794 pub fn archive_offset(&self) -> u64 {
796 self.archive_offset
797 }
798
799 pub fn path(&self) -> &Path {
801 &self.path
802 }
803
804 pub fn hi_block_table(&self) -> Option<&HiBlockTable> {
806 self.hi_block_table.as_ref()
807 }
808
809 fn validate_v4_md5_checksums(&mut self) -> Result<Option<Md5Status>> {
811 use md5::{Digest, Md5};
812
813 let v4_data = match &self.header.v4_data {
814 Some(data) => data,
815 None => return Ok(None),
816 };
817
818 let mut validate_table_md5 = |expected: &[u8; 16],
820 offset: u64,
821 size: u64|
822 -> Result<bool> {
823 if size == 0 {
824 return Ok(true); }
826
827 self.reader
829 .seek(SeekFrom::Start(self.archive_offset + offset))?;
830 let mut table_data = vec![0u8; size as usize];
831 match self.reader.read_exact(&mut table_data) {
832 Ok(_) => {
833 let mut hasher = Md5::new();
835 hasher.update(&table_data);
836 let actual_md5: [u8; 16] = hasher.finalize().into();
837
838 Ok(actual_md5 == *expected)
839 }
840 Err(e) => {
841 log::warn!(
842 "Failed to read table data for MD5 validation at offset 0x{:X}, size {}: {}",
843 self.archive_offset + offset,
844 size,
845 e
846 );
847 Ok(false)
848 }
849 }
850 };
851
852 let hash_table_valid = if self.header.hash_table_size > 0 {
854 let hash_offset = self.header.get_hash_table_pos();
855 let hash_size = v4_data.hash_table_size_64;
856 validate_table_md5(&v4_data.md5_hash_table, hash_offset, hash_size)?
857 } else {
858 true };
860
861 let block_table_valid = if self.header.block_table_size > 0 {
863 let block_offset = self.header.get_block_table_pos();
864 let block_size = v4_data.block_table_size_64;
865 validate_table_md5(&v4_data.md5_block_table, block_offset, block_size)?
866 } else {
867 true };
869
870 let hi_block_table_valid = if let Some(hi_pos) = self.header.hi_block_table_pos {
872 if hi_pos != 0 {
873 let hi_size = v4_data.hi_block_table_size_64;
874 validate_table_md5(&v4_data.md5_hi_block_table, hi_pos, hi_size)?
875 } else {
876 true
877 }
878 } else {
879 true };
881
882 let het_table_valid = if let Some(het_pos) = self.header.het_table_pos {
884 if het_pos != 0 {
885 let het_size = v4_data.het_table_size_64;
886 validate_table_md5(&v4_data.md5_het_table, het_pos, het_size)?
887 } else {
888 true
889 }
890 } else {
891 true };
893
894 let bet_table_valid = if let Some(bet_pos) = self.header.bet_table_pos {
896 if bet_pos != 0 {
897 let bet_size = v4_data.bet_table_size_64;
898 validate_table_md5(&v4_data.md5_bet_table, bet_pos, bet_size)?
899 } else {
900 true
901 }
902 } else {
903 true };
905
906 let header_valid = {
908 self.reader.seek(SeekFrom::Start(self.archive_offset))?;
909 let mut header_data = vec![0u8; 192];
910 match self.reader.read_exact(&mut header_data) {
911 Ok(_) => {
912 let mut hasher = Md5::new();
913 hasher.update(&header_data);
914 let actual_md5: [u8; 16] = hasher.finalize().into();
915
916 actual_md5 == v4_data.md5_mpq_header
917 }
918 Err(e) => {
919 log::warn!("Failed to read header for MD5 validation: {e}");
920 false
921 }
922 }
923 };
924
925 Ok(Some(Md5Status {
926 hash_table_valid,
927 block_table_valid,
928 hi_block_table_valid,
929 het_table_valid,
930 bet_table_valid,
931 header_valid,
932 }))
933 }
934
935 pub fn get_info(&mut self) -> Result<ArchiveInfo> {
937 log::debug!("Getting archive info");
938
939 if self.hash_table.is_none() && self.het_table.is_none() {
941 log::debug!("Loading tables for info");
942 self.load_tables()?;
943 }
944
945 log::debug!("Getting file size");
947 let file_size = self.reader.get_ref().metadata()?.len();
948
949 let file_count = if let Some(bet) = &self.bet_table {
951 bet.header.file_count as usize
952 } else if let Some(block_table) = &self.block_table {
953 block_table
955 .entries()
956 .iter()
957 .filter(|entry| entry.file_size != 0)
958 .count()
959 } else {
960 0
961 };
962
963 let max_file_count = if let Some(het) = &self.het_table {
965 het.header.max_file_count
966 } else {
967 self.header.hash_table_size
968 };
969
970 let has_listfile = self.find_file("(listfile)")?.is_some();
972 let has_signature = self.find_file("(signature)")?.is_some();
973 let has_attributes = self.attributes.is_some() || self.find_file("(attributes)")?.is_some();
974
975 let is_encrypted = if let Some(block_table) = &self.block_table {
977 use crate::tables::BlockEntry;
978 block_table
979 .entries()
980 .iter()
981 .any(|entry| (entry.flags & BlockEntry::FLAG_ENCRYPTED) != 0)
982 } else {
983 false
984 };
985
986 let signature_status = if has_signature {
988 match self.verify_signature() {
989 Ok(status) => status,
990 Err(e) => {
991 log::warn!("Failed to verify signature: {e}");
992 SignatureStatus::WeakInvalid
993 }
994 }
995 } else {
996 SignatureStatus::None
997 };
998
999 let hash_table_info = TableInfo {
1001 size: Some(self.header.hash_table_size),
1002 offset: self.header.get_hash_table_pos(),
1003 compressed_size: self.header.v4_data.as_ref().map(|v4| v4.hash_table_size_64),
1004 failed_to_load: self.hash_table.is_none() && self.header.hash_table_size > 0,
1005 };
1006
1007 let block_table_info = TableInfo {
1008 size: Some(self.header.block_table_size),
1009 offset: self.header.get_block_table_pos(),
1010 compressed_size: self
1011 .header
1012 .v4_data
1013 .as_ref()
1014 .map(|v4| v4.block_table_size_64),
1015 failed_to_load: self.block_table.is_none() && self.header.block_table_size > 0,
1016 };
1017
1018 let het_table_info = self.header.het_table_pos.and_then(|pos| {
1019 if pos == 0 {
1020 return None;
1021 }
1022
1023 let mut compressed_size = self.header.v4_data.as_ref().map(|v4| v4.het_table_size_64);
1025
1026 if compressed_size.is_none() && self.header.format_version == header::FormatVersion::V3
1028 {
1029 if let Ok(temp_reader) =
1031 std::fs::File::open(&self.path).map(std::io::BufReader::new)
1032 {
1033 let mut temp_archive = Self {
1034 path: self.path.clone(),
1035 reader: temp_reader,
1036 archive_offset: self.archive_offset,
1037 user_data: self.user_data.clone(),
1038 header: self.header.clone(),
1039 hash_table: None,
1040 block_table: None,
1041 hi_block_table: None,
1042 het_table: None,
1043 bet_table: None,
1044 attributes: None,
1045 };
1046
1047 if let Ok(size) = temp_archive.read_het_table_size(pos) {
1048 compressed_size = Some(size);
1049 }
1050 }
1051 }
1052
1053 Some(TableInfo {
1054 size: self.het_table.as_ref().map(|het| het.header.max_file_count),
1055 offset: pos,
1056 compressed_size,
1057 failed_to_load: self.het_table.is_none(),
1058 })
1059 });
1060
1061 let bet_table_info = self.header.bet_table_pos.and_then(|pos| {
1062 if pos == 0 {
1063 return None;
1064 }
1065
1066 let mut compressed_size = self.header.v4_data.as_ref().map(|v4| v4.bet_table_size_64);
1068
1069 if compressed_size.is_none() && self.header.format_version == header::FormatVersion::V3
1071 {
1072 if let Ok(temp_reader) =
1074 std::fs::File::open(&self.path).map(std::io::BufReader::new)
1075 {
1076 let mut temp_archive = Self {
1077 path: self.path.clone(),
1078 reader: temp_reader,
1079 archive_offset: self.archive_offset,
1080 user_data: self.user_data.clone(),
1081 header: self.header.clone(),
1082 hash_table: None,
1083 block_table: None,
1084 hi_block_table: None,
1085 het_table: None,
1086 bet_table: None,
1087 attributes: None,
1088 };
1089
1090 if let Ok(size) = temp_archive.read_bet_table_size(pos) {
1091 compressed_size = Some(size);
1092 }
1093 }
1094 }
1095
1096 Some(TableInfo {
1097 size: self.bet_table.as_ref().map(|bet| bet.header.file_count),
1098 offset: pos,
1099 compressed_size,
1100 failed_to_load: self.bet_table.is_none(),
1101 })
1102 });
1103
1104 let hi_block_table_info = self.header.hi_block_table_pos.and_then(|pos| {
1105 if pos == 0 {
1106 return None;
1107 }
1108
1109 Some(TableInfo {
1110 size: if self.hi_block_table.is_some() {
1111 Some(self.header.block_table_size)
1112 } else {
1113 None
1114 },
1115 offset: pos,
1116 compressed_size: self
1117 .header
1118 .v4_data
1119 .as_ref()
1120 .map(|v4| v4.hi_block_table_size_64),
1121 failed_to_load: self.hi_block_table.is_none(),
1122 })
1123 });
1124
1125 let user_data_info = self.user_data.as_ref().map(|ud| UserDataInfo {
1126 header_size: ud.user_data_header_size,
1127 data_size: ud.user_data_size,
1128 });
1129
1130 let md5_status = if self.header.v4_data.is_some() {
1132 self.validate_v4_md5_checksums()?
1133 } else {
1134 None
1135 };
1136
1137 Ok(ArchiveInfo {
1138 path: self.path.clone(),
1139 file_size,
1140 archive_offset: self.archive_offset,
1141 format_version: self.header.format_version,
1142 file_count,
1143 max_file_count,
1144 sector_size: self.header.sector_size(),
1145 is_encrypted,
1146 has_signature,
1147 signature_status,
1148 hash_table_info,
1149 block_table_info,
1150 het_table_info,
1151 bet_table_info,
1152 hi_block_table_info,
1153 has_attributes,
1154 has_listfile,
1155 user_data_info,
1156 md5_status,
1157 })
1158 }
1159
1160 pub fn hash_table(&self) -> Option<&HashTable> {
1162 self.hash_table.as_ref()
1163 }
1164
1165 pub fn block_table(&self) -> Option<&BlockTable> {
1167 self.block_table.as_ref()
1168 }
1169
1170 pub fn het_table(&self) -> Option<&HetTable> {
1172 self.het_table.as_ref()
1173 }
1174
1175 pub fn bet_table(&self) -> Option<&BetTable> {
1177 self.bet_table.as_ref()
1178 }
1179
1180 pub fn find_file(&self, filename: &str) -> Result<Option<FileInfo>> {
1182 let is_special_file = matches!(
1184 filename,
1185 "(listfile)" | "(attributes)" | "(signature)" | "(patch_metadata)"
1186 );
1187
1188 if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table) {
1190 if het.header.max_file_count > 0 && bet.header.file_count > 0 {
1192 let (_file_index_opt, collision_candidates) =
1193 het.find_file_with_collision_info(filename);
1194
1195 if !collision_candidates.is_empty() {
1198 if collision_candidates.len() > 1 {
1199 log::debug!(
1200 "HET: '{}' has {} collision candidates, verifying against BET hashes",
1201 filename,
1202 collision_candidates.len()
1203 );
1204 }
1205
1206 for &candidate_index in &collision_candidates {
1208 if bet.verify_file_hash(candidate_index, filename) {
1210 if let Some(bet_info) = bet.get_file_info(candidate_index) {
1212 log::debug!(
1213 "HET/BET: Found '{}' at file_index={} (verified by BET hash)",
1214 filename,
1215 candidate_index
1216 );
1217 return Ok(Some(FileInfo {
1218 filename: filename.to_string(),
1219 hash_index: 0, block_index: candidate_index as usize,
1221 file_pos: self.archive_offset + bet_info.file_pos,
1222 compressed_size: bet_info.compressed_size,
1223 file_size: bet_info.file_size,
1224 flags: bet_info.flags,
1225 locale: 0, }));
1227 }
1228 }
1229 }
1230
1231 log::debug!(
1233 "HET/BET: '{}' not found - {} candidates checked, none matched BET hash",
1234 filename,
1235 collision_candidates.len()
1236 );
1237 }
1238
1239 if !is_special_file && (self.hash_table.is_none() || self.block_table.is_none()) {
1242 return Ok(None);
1243 }
1244 }
1245 }
1246
1247 self.find_file_classic(filename)
1253 }
1254
1255 fn find_file_classic(&self, filename: &str) -> Result<Option<FileInfo>> {
1257 let hash_table = match self.hash_table.as_ref() {
1260 Some(table) => table,
1261 None => return Ok(None),
1262 };
1263 let block_table = match self.block_table.as_ref() {
1264 Some(table) => table,
1265 None => return Ok(None),
1266 };
1267
1268 if let Some((hash_index, hash_entry)) = hash_table.find_file(filename, 0) {
1270 let block_entry = block_table
1271 .get(hash_entry.block_index as usize)
1272 .ok_or_else(|| Error::block_table("Invalid block index"))?;
1273
1274 let file_pos = if let Some(hi_block) = &self.hi_block_table {
1276 let high_bits = hi_block.get_file_pos_high(hash_entry.block_index as usize);
1277 (high_bits << 32) | (block_entry.file_pos as u64)
1278 } else {
1279 block_entry.file_pos as u64
1280 };
1281
1282 Ok(Some(FileInfo {
1283 filename: filename.to_string(),
1284 hash_index,
1285 block_index: hash_entry.block_index as usize,
1286 file_pos: self.archive_offset + file_pos,
1287 compressed_size: block_entry.compressed_size as u64,
1288 file_size: block_entry.file_size as u64,
1289 flags: block_entry.flags,
1290 locale: hash_entry.locale,
1291 }))
1292 } else {
1293 Ok(None)
1294 }
1295 }
1296
1297 pub fn list(&mut self) -> Result<Vec<FileEntry>> {
1299 if let Some(_listfile_info) = self.find_file("(listfile)")? {
1301 match self.read_file("(listfile)") {
1303 Ok(listfile_data) => {
1304 match special_files::parse_listfile(&listfile_data) {
1306 Ok(filenames) => {
1307 let mut entries = Vec::new();
1308
1309 for filename in filenames {
1311 if let Some(file_info) = self.find_file(&filename)? {
1312 entries.push(FileEntry {
1313 name: filename,
1314 size: file_info.file_size,
1315 compressed_size: file_info.compressed_size,
1316 flags: file_info.flags,
1317 hashes: None,
1318 table_indices: Some((
1319 file_info.hash_index,
1320 Some(file_info.block_index),
1321 )),
1322 });
1323 } else {
1324 log::warn!(
1326 "File '{filename}' listed in (listfile) but not found in archive"
1327 );
1328 }
1329 }
1330
1331 return Ok(entries);
1332 }
1333 Err(e) => {
1334 log::warn!(
1335 "Failed to parse (listfile): {e}. Falling back to anonymous enumeration."
1336 );
1337 }
1338 }
1339 }
1340 Err(e) => {
1341 log::warn!(
1342 "Failed to read (listfile): {e}. Falling back to anonymous enumeration."
1343 );
1344 }
1345 }
1346 }
1347
1348 log::info!("Enumerating anonymous entries");
1350
1351 let mut entries = Vec::new();
1352
1353 if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table)
1355 && het.header.max_file_count > 0
1356 && bet.header.file_count > 0
1357 {
1358 log::info!("Enumerating files using HET/BET tables");
1359
1360 for i in 0..bet.header.file_count {
1362 if let Some(bet_info) = bet.get_file_info(i) {
1363 if bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0 {
1365 entries.push(FileEntry {
1366 name: format!("file_{i:08}.dat"), size: bet_info.file_size,
1368 compressed_size: bet_info.compressed_size,
1369 flags: bet_info.flags,
1370 hashes: None,
1371 table_indices: Some((i as usize, None)), });
1373 }
1374 }
1375 }
1376
1377 if !entries.is_empty() {
1379 return Ok(entries);
1380 }
1381 }
1382
1383 let hash_table = self
1385 .hash_table
1386 .as_ref()
1387 .ok_or_else(|| Error::invalid_format("No tables loaded for enumeration"))?;
1388 let block_table = self
1389 .block_table
1390 .as_ref()
1391 .ok_or_else(|| Error::invalid_format("No block table loaded"))?;
1392
1393 log::info!("Enumerating files using hash/block tables");
1394
1395 for (i, hash_entry) in hash_table.entries().iter().enumerate() {
1397 if hash_entry.is_valid()
1398 && let Some(block_entry) = block_table.get(hash_entry.block_index as usize)
1399 && block_entry.exists()
1400 {
1401 entries.push(FileEntry {
1402 name: format!("file_{i:08}.dat"), size: block_entry.file_size as u64,
1404 compressed_size: block_entry.compressed_size as u64,
1405 flags: block_entry.flags,
1406 hashes: None,
1407 table_indices: Some((i, Some(hash_entry.block_index as usize))), });
1409 }
1410 }
1411
1412 Ok(entries)
1413 }
1414
1415 pub fn list_all(&mut self) -> Result<Vec<FileEntry>> {
1418 let mut entries = Vec::new();
1419
1420 if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table)
1422 && het.header.max_file_count > 0
1423 && bet.header.file_count > 0
1424 {
1425 log::info!("Enumerating all files using HET/BET tables");
1426
1427 for i in 0..bet.header.file_count {
1429 if let Some(bet_info) = bet.get_file_info(i) {
1430 if bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0 {
1432 entries.push(FileEntry {
1433 name: format!("file_{i:08}.dat"), size: bet_info.file_size,
1435 compressed_size: bet_info.compressed_size,
1436 flags: bet_info.flags,
1437 hashes: None,
1438 table_indices: Some((i as usize, None)), });
1440 }
1441 }
1442 }
1443
1444 if !entries.is_empty() {
1446 return Ok(entries);
1447 }
1448 }
1449
1450 let hash_table = self
1452 .hash_table
1453 .as_ref()
1454 .ok_or_else(|| Error::invalid_format("No tables loaded for enumeration"))?;
1455 let block_table = self
1456 .block_table
1457 .as_ref()
1458 .ok_or_else(|| Error::invalid_format("No block table loaded"))?;
1459
1460 log::info!("Enumerating all files using hash/block tables");
1461
1462 let mut block_indices_seen = std::collections::HashSet::new();
1464
1465 for hash_entry in hash_table.entries().iter() {
1466 if hash_entry.is_valid() {
1467 let block_index = hash_entry.block_index as usize;
1468
1469 if !block_indices_seen.insert(block_index) {
1471 continue;
1472 }
1473
1474 if let Some(block_entry) = block_table.get(block_index)
1475 && block_entry.exists()
1476 {
1477 entries.push(FileEntry {
1478 name: format!("file_{block_index:08}.dat"),
1479 size: block_entry.file_size as u64,
1480 compressed_size: block_entry.compressed_size as u64,
1481 flags: block_entry.flags,
1482 hashes: None,
1483 table_indices: Some((0, Some(block_index))), });
1485 }
1486 }
1487 }
1488
1489 entries.sort_by(|a, b| a.name.cmp(&b.name));
1491
1492 Ok(entries)
1493 }
1494
1495 pub fn list_with_hashes(&mut self) -> Result<Vec<FileEntry>> {
1497 let mut entries = self.list()?;
1498
1499 for entry in &mut entries {
1501 let hash1 = crate::crypto::hash_string(&entry.name, crate::crypto::hash_type::NAME_A);
1502 let hash2 = crate::crypto::hash_string(&entry.name, crate::crypto::hash_type::NAME_B);
1503 entry.hashes = Some((hash1, hash2));
1504 }
1505
1506 Ok(entries)
1507 }
1508
1509 pub fn list_with_db(&mut self, db: &crate::database::Database) -> Result<Vec<FileEntry>> {
1511 use crate::database::HashLookup;
1512
1513 let mut entries = self.list_all_with_hashes()?;
1515
1516 for entry in &mut entries {
1518 if let Some((hash_a, hash_b)) = entry.hashes
1519 && let Ok(Some(filename)) = db.lookup_filename(hash_a, hash_b)
1520 {
1521 entry.name = filename;
1522 }
1523 }
1524
1525 Ok(entries)
1526 }
1527
1528 pub fn record_listfile_to_db(&mut self, db: &crate::database::Database) -> Result<usize> {
1530 use crate::database::HashLookup;
1531
1532 if let Some(_listfile_info) = self.find_file("(listfile)")?
1534 && let Ok(listfile_data) = self.read_file("(listfile)")
1535 && let Ok(filenames) = special_files::parse_listfile(&listfile_data)
1536 {
1537 let source = format!("archive:{}", self.path.display());
1539 let filenames_with_source: Vec<(&str, Option<&str>)> = filenames
1540 .iter()
1541 .map(|f| (f.as_str(), Some(source.as_str())))
1542 .collect();
1543
1544 match db.store_filenames(&filenames_with_source) {
1545 Ok((new_count, updated_count)) => {
1546 log::info!(
1547 "Recorded {new_count} new and {updated_count} updated filenames from listfile to database"
1548 );
1549 return Ok(new_count + updated_count);
1550 }
1551 Err(e) => {
1552 log::error!("Failed to store filenames in database: {e}");
1553 return Err(Error::Crypto(format!("Database error: {e}")));
1554 }
1555 }
1556 }
1557
1558 Ok(0)
1559 }
1560
1561 pub fn list_all_with_hashes(&mut self) -> Result<Vec<FileEntry>> {
1563 let mut entries = Vec::new();
1564
1565 if let (Some(het), Some(bet)) = (&self.het_table, &self.bet_table)
1567 && het.header.max_file_count > 0
1568 && bet.header.file_count > 0
1569 {
1570 log::info!("Enumerating all files using HET/BET tables with hashes");
1571
1572 for i in 0..bet.header.file_count {
1574 if let Some(bet_info) = bet.get_file_info(i)
1575 && bet_info.flags & crate::tables::BlockEntry::FLAG_EXISTS != 0
1576 {
1577 entries.push(FileEntry {
1578 name: format!("file_{i:08}.dat"),
1579 size: bet_info.file_size,
1580 compressed_size: bet_info.compressed_size,
1581 flags: bet_info.flags,
1582 hashes: None, table_indices: Some((i as usize, None)), });
1585 }
1586 }
1587
1588 if !entries.is_empty() {
1589 return Ok(entries);
1590 }
1591 }
1592
1593 let hash_table = self
1595 .hash_table
1596 .as_ref()
1597 .ok_or_else(|| Error::invalid_format("No tables loaded for enumeration"))?;
1598 let block_table = self
1599 .block_table
1600 .as_ref()
1601 .ok_or_else(|| Error::invalid_format("No block table loaded"))?;
1602
1603 log::info!("Enumerating all files using hash/block tables with hashes");
1604
1605 let mut block_indices_seen = std::collections::HashSet::new();
1607
1608 for hash_entry in hash_table.entries().iter() {
1609 if hash_entry.is_valid() {
1610 let block_index = hash_entry.block_index as usize;
1611
1612 if !block_indices_seen.insert(block_index) {
1613 continue;
1614 }
1615
1616 if let Some(block_entry) = block_table.get(block_index)
1617 && block_entry.exists()
1618 {
1619 entries.push(FileEntry {
1620 name: format!("file_{block_index:08}.dat"),
1621 size: block_entry.file_size as u64,
1622 compressed_size: block_entry.compressed_size as u64,
1623 flags: block_entry.flags,
1624 hashes: Some((hash_entry.name_1, hash_entry.name_2)),
1625 table_indices: Some((0, Some(block_index))), });
1627 }
1628 }
1629 }
1630
1631 entries.sort_by(|a, b| a.name.cmp(&b.name));
1633
1634 Ok(entries)
1635 }
1636
1637 pub fn read_file(&mut self, name: &str) -> Result<Vec<u8>> {
1639 let file_info = self
1640 .find_file(name)?
1641 .ok_or_else(|| Error::FileNotFound(name.to_string()))?;
1642
1643 if file_info.is_patch_file() {
1645 return Err(Error::OperationNotSupported {
1646 version: self.header.format_version as u16,
1647 operation: format!(
1648 "Reading patch file '{name}' directly. Patch files contain binary patches that must be applied to base files."
1649 ),
1650 });
1651 }
1652
1653 let (file_size_for_key, actual_file_size) =
1656 if self.het_table.is_some() && self.bet_table.is_some() {
1657 (file_info.file_size as u32, file_info.file_size)
1659 } else {
1660 let block_table = self
1662 .block_table
1663 .as_ref()
1664 .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
1665 let block_entry = block_table
1666 .get(file_info.block_index)
1667 .ok_or_else(|| Error::block_table("Invalid block index"))?;
1668 (block_entry.file_size, block_entry.file_size as u64)
1669 };
1670
1671 let key = if file_info.is_encrypted() {
1673 let base_key = hash_string(name, hash_type::FILE_KEY);
1674 if file_info.has_fix_key() {
1675 let file_pos = (file_info.file_pos - self.archive_offset) as u32;
1677 (base_key.wrapping_add(file_pos)) ^ file_size_for_key
1678 } else {
1679 base_key
1680 }
1681 } else {
1682 0
1683 };
1684
1685 self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
1687
1688 if file_info.is_single_unit() || !file_info.is_compressed() {
1689 let mut data = vec![0u8; file_info.compressed_size as usize];
1691 self.reader.read_exact(&mut data)?;
1692
1693 if file_info.is_encrypted() {
1695 log::debug!(
1696 "Decrypting file data: key=0x{:08X}, size={}",
1697 key,
1698 data.len()
1699 );
1700 if data.len() <= 64 {
1701 log::debug!("Before decrypt: {:02X?}", &data);
1702 }
1703 decrypt_file_data(&mut data, key);
1704 if data.len() <= 64 {
1705 log::debug!("After decrypt: {:02X?}", &data);
1706 }
1707 }
1708
1709 if file_info.has_sector_crc() && file_info.is_single_unit() {
1711 let mut crc_bytes = [0u8; 4];
1713 self.reader.read_exact(&mut crc_bytes)?;
1714 let expected_crc = u32::from_le_bytes(crc_bytes);
1715
1716 let data_to_check = if file_info.is_compressed() {
1718 let compression_type = data[0];
1720 let compressed_data = &data[1..];
1721 compression::decompress(
1722 compressed_data,
1723 compression_type,
1724 actual_file_size as usize,
1725 )?
1726 } else {
1727 data.clone()
1728 };
1729
1730 let actual_crc = adler2::adler32_slice(&data_to_check);
1732 if actual_crc != expected_crc {
1733 return Err(Error::ChecksumMismatch {
1734 file: name.to_string(),
1735 expected: expected_crc,
1736 actual: actual_crc,
1737 });
1738 }
1739
1740 log::debug!("Single unit file CRC validated: 0x{actual_crc:08X}");
1741 }
1742
1743 if file_info.is_compressed() {
1745 if file_info.is_single_unit() {
1746 if data.len() == actual_file_size as usize {
1752 log::debug!(
1753 "SINGLE_UNIT file has equal compressed/uncompressed size ({} bytes), trying uncompressed first",
1754 data.len()
1755 );
1756
1757 Ok(data)
1760 } else if let Some(compression_method) = file_info.get_compression_method() {
1761 if !data.is_empty() {
1764 let actual_compression_method = data[0];
1765 let compressed_data = &data[1..];
1766
1767 log::debug!(
1768 "Decompressing SINGLE_UNIT file: method_from_flags=0x{:02X}, actual_method_byte=0x{:02X}, compressed_size={}, expected_size={}",
1769 compression_method,
1770 actual_compression_method,
1771 compressed_data.len(),
1772 actual_file_size
1773 );
1774
1775 compression::decompress(
1778 compressed_data,
1779 actual_compression_method,
1780 actual_file_size as usize,
1781 )
1782 } else {
1783 Err(Error::compression("Empty compressed data"))
1784 }
1785 } else {
1786 Err(Error::compression(
1787 "Could not determine compression method from flags",
1788 ))
1789 }
1790 } else {
1791 log::warn!("Non-single-unit compressed file in single-unit code path");
1794 Ok(data)
1795 }
1796 } else {
1797 if file_info.is_encrypted() && data.len() > actual_file_size as usize {
1799 data.truncate(actual_file_size as usize);
1800 }
1801 Ok(data)
1802 }
1803 } else {
1804 self.read_sectored_file(&file_info, key)
1806 }
1807 }
1808
1809 pub(crate) fn read_patch_file_raw(&mut self, name: &str) -> Result<Vec<u8>> {
1817 let file_info = self
1818 .find_file(name)?
1819 .ok_or_else(|| Error::FileNotFound(name.to_string()))?;
1820
1821 if !file_info.is_patch_file() {
1823 return Err(Error::invalid_format(format!(
1824 "File '{name}' is not a patch file"
1825 )));
1826 }
1827
1828 let (file_size_for_key, _actual_file_size) =
1831 if self.het_table.is_some() && self.bet_table.is_some() {
1832 (file_info.file_size as u32, file_info.file_size)
1834 } else {
1835 let block_table = self
1837 .block_table
1838 .as_ref()
1839 .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
1840 let block_entry = block_table
1841 .get(file_info.block_index)
1842 .ok_or_else(|| Error::block_table("Invalid block index"))?;
1843 (block_entry.file_size, block_entry.file_size as u64)
1844 };
1845
1846 let key = if file_info.is_encrypted() {
1848 let base_key = hash_string(name, hash_type::FILE_KEY);
1849 if file_info.has_fix_key() {
1850 let file_pos = (file_info.file_pos - self.archive_offset) as u32;
1852 (base_key.wrapping_add(file_pos)) ^ file_size_for_key
1853 } else {
1854 base_key
1855 }
1856 } else {
1857 0
1858 };
1859
1860 self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
1863
1864 let mut patch_info_buf = [0u8; 28];
1866 self.reader.read_exact(&mut patch_info_buf)?;
1867
1868 let patch_info_length = u32::from_le_bytes([
1869 patch_info_buf[0],
1870 patch_info_buf[1],
1871 patch_info_buf[2],
1872 patch_info_buf[3],
1873 ]);
1874 let patch_info_flags = u32::from_le_bytes([
1875 patch_info_buf[4],
1876 patch_info_buf[5],
1877 patch_info_buf[6],
1878 patch_info_buf[7],
1879 ]);
1880 let patch_data_size = u32::from_le_bytes([
1881 patch_info_buf[8],
1882 patch_info_buf[9],
1883 patch_info_buf[10],
1884 patch_info_buf[11],
1885 ]);
1886
1887 log::debug!(
1888 "TPatchInfo: length={}, flags=0x{:08X}, data_size={} bytes",
1889 patch_info_length,
1890 patch_info_flags,
1891 patch_data_size
1892 );
1893
1894 let actual_patch_size = patch_data_size as usize;
1896
1897 let is_single_unit = file_info.is_single_unit();
1900
1901 if is_single_unit {
1902 log::debug!("Patch file is stored as single unit");
1903 let compressed_data_size =
1904 file_info.compressed_size as usize - patch_info_length as usize;
1905
1906 let mut data = vec![0u8; compressed_data_size];
1907 self.reader.read_exact(&mut data)?;
1908
1909 log::debug!(
1910 "Read {} bytes of compressed patch data (single unit)",
1911 data.len()
1912 );
1913 log::debug!("First 32 bytes: {:02X?}", &data[..32.min(data.len())]);
1914
1915 if file_info.is_encrypted() {
1917 log::debug!(
1918 "Decrypting patch file data: key=0x{:08X}, size={}",
1919 key,
1920 data.len()
1921 );
1922 decrypt_file_data(&mut data, key);
1923 }
1924
1925 if file_info.is_compressed() {
1927 let compression_type = data[0];
1928 let compressed_data = &data[1..];
1929
1930 log::debug!(
1931 "Decompressing patch file (single unit): method=0x{:02X}, compressed={} bytes → {} bytes",
1932 compression_type,
1933 compressed_data.len(),
1934 actual_patch_size
1935 );
1936
1937 compression::decompress(compressed_data, compression_type, actual_patch_size)
1938 } else {
1939 Ok(data)
1940 }
1941 } else {
1942 log::debug!("Patch file is sectored, reading with modified sector handling");
1944
1945 let sector_size = self.header.sector_size();
1947 let sector_count = (patch_data_size as usize).div_ceil(sector_size);
1948
1949 log::debug!(
1950 "Patch sectors: data_size={}, sector_size={}, sector_count={}",
1951 patch_data_size,
1952 sector_size,
1953 sector_count
1954 );
1955
1956 let offset_table_size = (sector_count + 1) * 4;
1958 let mut offset_data = vec![0u8; offset_table_size];
1959 self.reader.read_exact(&mut offset_data)?;
1960
1961 log::debug!(
1962 "Read sector offset table: {} bytes for {} sectors",
1963 offset_table_size,
1964 sector_count
1965 );
1966
1967 let mut sector_offsets = Vec::with_capacity(sector_count + 1);
1969 let mut cursor = std::io::Cursor::new(&offset_data);
1970 for _ in 0..=sector_count {
1971 sector_offsets.push(cursor.read_u32::<LittleEndian>()?);
1972 }
1973
1974 log::debug!("Sector offsets: {:?}", §or_offsets);
1975
1976 let mut decompressed_data = Vec::with_capacity(patch_data_size as usize);
1978
1979 for i in 0..sector_count {
1980 let sector_start = sector_offsets[i] as usize;
1981 let sector_end = sector_offsets[i + 1] as usize;
1982 let sector_compressed_size = sector_end - sector_start;
1983
1984 log::debug!(
1985 "Reading sector {}: offset={}, size={} bytes",
1986 i,
1987 sector_start,
1988 sector_compressed_size
1989 );
1990
1991 let sector_file_pos =
1994 file_info.file_pos + patch_info_length as u64 + sector_start as u64;
1995
1996 self.reader.seek(SeekFrom::Start(sector_file_pos))?;
1997
1998 let mut sector_data = vec![0u8; sector_compressed_size];
1999 self.reader.read_exact(&mut sector_data)?;
2000
2001 log::debug!(
2002 "Sector {} data first 16 bytes: {:02X?}",
2003 i,
2004 §or_data[..16.min(sector_data.len())]
2005 );
2006
2007 let compression_method = sector_data[0];
2010 log::debug!(
2011 "Decompressing sector {} with method 0x{:02X} ({} bytes compressed)",
2012 i,
2013 compression_method,
2014 sector_data.len() - 1
2015 );
2016
2017 let expected_size =
2019 sector_size.min(patch_data_size as usize - decompressed_data.len());
2020 let sector_decompressed = compression::decompress(
2021 §or_data[1..], compression_method,
2023 expected_size,
2024 )?;
2025
2026 log::debug!(
2027 "Sector {} decompressed to {} bytes",
2028 i,
2029 sector_decompressed.len()
2030 );
2031
2032 decompressed_data.extend_from_slice(§or_decompressed);
2033 }
2034
2035 log::debug!(
2036 "Successfully decompressed {} bytes from {} sectors",
2037 decompressed_data.len(),
2038 sector_count
2039 );
2040
2041 Ok(decompressed_data)
2042 }
2043 }
2044
2045 pub fn read_file_by_indices(
2047 &mut self,
2048 hash_index: usize,
2049 block_index: Option<usize>,
2050 ) -> Result<Vec<u8>> {
2051 let file_info = if let Some(block_idx) = block_index {
2052 let hash_table = self
2054 .hash_table
2055 .as_ref()
2056 .ok_or_else(|| Error::invalid_format("Hash table not loaded"))?;
2057 let block_table = self
2058 .block_table
2059 .as_ref()
2060 .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
2061
2062 let hash_entry = hash_table
2063 .entries()
2064 .get(hash_index)
2065 .ok_or_else(|| Error::hash_table("Invalid hash index"))?;
2066 let block_entry = block_table
2067 .get(block_idx)
2068 .ok_or_else(|| Error::block_table("Invalid block index"))?;
2069
2070 let file_pos = if let Some(hi_block) = &self.hi_block_table {
2072 let high_bits = hi_block.get_file_pos_high(block_idx);
2073 (high_bits << 32) | (block_entry.file_pos as u64)
2074 } else {
2075 block_entry.file_pos as u64
2076 };
2077
2078 FileInfo {
2079 filename: format!("file_{hash_index:08}.dat"),
2080 hash_index,
2081 block_index: block_idx,
2082 file_pos: self.archive_offset + file_pos,
2083 compressed_size: block_entry.compressed_size as u64,
2084 file_size: block_entry.file_size as u64,
2085 flags: block_entry.flags,
2086 locale: hash_entry.locale,
2087 }
2088 } else {
2089 let bet = self
2091 .bet_table
2092 .as_ref()
2093 .ok_or_else(|| Error::invalid_format("BET table not loaded"))?;
2094
2095 let bet_info = bet
2096 .get_file_info(hash_index as u32)
2097 .ok_or_else(|| Error::invalid_format("Invalid file index"))?;
2098
2099 let file_pos = self.archive_offset + bet_info.file_pos;
2101
2102 FileInfo {
2103 filename: format!("file_{hash_index:08}.dat"),
2104 hash_index: 0, block_index: 0, file_pos,
2107 compressed_size: bet_info.compressed_size,
2108 file_size: bet_info.file_size,
2109 flags: bet_info.flags,
2110 locale: 0, }
2112 };
2113
2114 if file_info.is_patch_file() {
2116 return Err(Error::OperationNotSupported {
2117 version: self.header.format_version as u16,
2118 operation: format!(
2119 "Reading patch file '{}' directly. Patch files contain binary patches that must be applied to base files.",
2120 file_info.filename
2121 ),
2122 });
2123 }
2124
2125 let key = if file_info.is_encrypted() {
2129 hash_string(&file_info.filename, hash_type::FILE_KEY)
2131 } else {
2132 0
2133 };
2134
2135 let (file_size_for_key, actual_file_size) =
2137 if self.het_table.is_some() && self.bet_table.is_some() {
2138 (file_info.file_size as u32, file_info.file_size)
2140 } else {
2141 let block_table = self
2143 .block_table
2144 .as_ref()
2145 .ok_or_else(|| Error::invalid_format("Block table not loaded"))?;
2146 let block_entry = block_table
2147 .get(file_info.block_index)
2148 .ok_or_else(|| Error::block_table("Invalid block index"))?;
2149 (block_entry.file_size, block_entry.file_size as u64)
2150 };
2151
2152 let key = if file_info.is_encrypted() && file_info.has_fix_key() {
2154 key.wrapping_add(file_size_for_key)
2155 } else {
2156 key
2157 };
2158
2159 self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
2161
2162 if file_info.is_single_unit() || !file_info.is_compressed() {
2163 let mut data = vec![0u8; file_info.compressed_size as usize];
2165 self.reader.read_exact(&mut data)?;
2166
2167 if file_info.is_encrypted() {
2169 log::debug!(
2170 "Decrypting file data: key=0x{:08X}, size={}",
2171 key,
2172 data.len()
2173 );
2174 decrypt_file_data(&mut data, key);
2175 }
2176
2177 if file_info.is_compressed() {
2179 if data.is_empty() {
2180 return Err(Error::compression("File data is empty"));
2181 }
2182
2183 if file_info.is_implode() {
2185 log::debug!(
2186 "Decompressing single unit IMPLODE file: input_size={}, target_size={}",
2187 data.len(),
2188 actual_file_size
2189 );
2190 compression::decompress(&data, 0x08, actual_file_size as usize)
2191 } else {
2192 let compression_type = data[0];
2194 let compressed_data = &data[1..];
2195
2196 log::debug!(
2197 "Decompressing single unit file: method=0x{:02X}, input_size={}, target_size={}, first bytes: {:02X?}",
2198 compression_type,
2199 compressed_data.len(),
2200 actual_file_size,
2201 &compressed_data[..compressed_data.len().min(16)]
2202 );
2203
2204 compression::decompress(
2205 compressed_data,
2206 compression_type,
2207 actual_file_size as usize,
2208 )
2209 }
2210 } else {
2211 Ok(data)
2212 }
2213 } else {
2214 self.read_sectored_file(&file_info, key)
2216 }
2217 }
2218
2219 fn read_sectored_file(&mut self, file_info: &FileInfo, key: u32) -> Result<Vec<u8>> {
2221 let sector_size = self.header.sector_size();
2222 let sector_count = (file_info.file_size as usize).div_ceil(sector_size);
2223
2224 log::debug!("Reading sectored file:");
2225 log::debug!(" file_size: {} bytes", file_info.file_size);
2226 log::debug!(" compressed_size: {} bytes", file_info.compressed_size);
2227 log::debug!(" sector_size: {} bytes", sector_size);
2228 log::debug!(" sector_count: {}", sector_count);
2229 log::debug!(" is_patch_file: {}", file_info.is_patch_file());
2230
2231 self.reader.seek(SeekFrom::Start(file_info.file_pos))?;
2233 let offset_table_size = (sector_count + 1) * 4;
2234 log::debug!(" offset_table_size: {} bytes", offset_table_size);
2235 log::debug!(
2236 " Attempting to read offset table at position 0x{:X}",
2237 file_info.file_pos
2238 );
2239
2240 let mut offset_data = vec![0u8; offset_table_size];
2241 self.reader.read_exact(&mut offset_data).map_err(|e| {
2242 log::error!("Failed to read offset table: {}", e);
2243 log::error!(
2244 " Tried to read {} bytes at position 0x{:X}",
2245 offset_table_size,
2246 file_info.file_pos
2247 );
2248 e
2249 })?;
2250
2251 if file_info.is_encrypted() {
2253 let offset_key = key.wrapping_sub(1);
2254 decrypt_file_data(&mut offset_data, offset_key);
2255 }
2256
2257 let mut sector_offsets = Vec::with_capacity(sector_count + 1);
2259 let mut cursor = std::io::Cursor::new(&offset_data);
2260 for _ in 0..=sector_count {
2261 sector_offsets.push(cursor.read_u32::<LittleEndian>()?);
2262 }
2263
2264 log::debug!(
2265 "Sector offsets: first={}, last={}",
2266 sector_offsets.first().copied().unwrap_or(0),
2267 sector_offsets.last().copied().unwrap_or(0)
2268 );
2269
2270 let mut sector_crcs = None;
2272 if file_info.has_sector_crc() {
2273 let first_data_offset = sector_offsets[0] as usize;
2276 let expected_crc_table_start = offset_table_size;
2277 let expected_crc_table_size = sector_count * 4;
2278
2279 if first_data_offset >= expected_crc_table_start + expected_crc_table_size {
2280 let mut crc_data = vec![0u8; expected_crc_table_size];
2282 self.reader.read_exact(&mut crc_data)?;
2283
2284 if file_info.is_encrypted() {
2287 let crc_key = key.wrapping_sub(1).wrapping_add(sector_count as u32);
2288 decrypt_file_data(&mut crc_data, crc_key);
2289 }
2290
2291 let mut crcs = Vec::with_capacity(sector_count);
2292 let mut cursor = std::io::Cursor::new(&crc_data);
2293 for _ in 0..sector_count {
2294 crcs.push(cursor.read_u32::<LittleEndian>()?);
2295 }
2296
2297 log::debug!(
2299 "Read {} sector CRCs, first few: {:?}",
2300 sector_count,
2301 &crcs[..5.min(crcs.len())]
2302 );
2303
2304 sector_crcs = Some(crcs);
2305 } else {
2306 log::debug!(
2307 "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.",
2308 offset_table_size,
2309 first_data_offset,
2310 expected_crc_table_start + expected_crc_table_size
2311 );
2312 }
2313 }
2314
2315 let mut decompressed_data = Vec::with_capacity(file_info.file_size as usize);
2317
2318 let max_sector_size = sector_size + 1024;
2321 let mut sector_buffer = vec![0u8; max_sector_size];
2322
2323 for i in 0..sector_count {
2324 let sector_start = sector_offsets[i] as u64;
2325 let sector_end = sector_offsets[i + 1] as u64;
2326
2327 if sector_end < sector_start {
2328 log::warn!(
2331 "Invalid sector offsets detected: start={sector_start}, end={sector_end} for sector {i}. Attempting recovery."
2332 );
2333
2334 let remaining = file_info.file_size as usize - decompressed_data.len();
2336 let expected_size = remaining.min(sector_size);
2337 decompressed_data.extend(vec![0u8; expected_size]);
2338 continue;
2339 }
2340
2341 let sector_size_compressed = (sector_end - sector_start) as usize;
2342
2343 let remaining = file_info.file_size as usize - decompressed_data.len();
2345 let expected_size = remaining.min(sector_size);
2346
2347 self.reader
2349 .seek(SeekFrom::Start(file_info.file_pos + sector_start))?;
2350
2351 if sector_size_compressed > sector_buffer.len() {
2353 sector_buffer.resize(sector_size_compressed, 0);
2354 }
2355
2356 let sector_data = &mut sector_buffer[..sector_size_compressed];
2358 self.reader.read_exact(sector_data)?;
2359
2360 if i == 0 {
2361 log::debug!(
2362 "First sector: offset={}, size={}, first 16 bytes: {:02X?}",
2363 sector_start,
2364 sector_size_compressed,
2365 §or_data[..16.min(sector_data.len())]
2366 );
2367 }
2368
2369 if file_info.is_encrypted() {
2371 let sector_key = key.wrapping_add(i as u32);
2372 decrypt_file_data(sector_data, sector_key);
2373 }
2374
2375 if let Some(ref _crcs) = sector_crcs {
2378 log::trace!("Skipping CRC validation for sector {i}");
2381 }
2382
2383 let decompressed_sector = if file_info.is_compressed()
2385 && sector_size_compressed < expected_size
2386 {
2387 if !sector_data.is_empty() {
2388 if file_info.is_implode() {
2390 match compression::decompress(sector_data, 0x08, expected_size) {
2392 Ok(decompressed) => decompressed,
2393 Err(e) => {
2394 log::warn!(
2395 "Failed to decompress IMPLODE sector {i}: {e}. Using zeros."
2396 );
2397 vec![0u8; expected_size]
2398 }
2399 }
2400 } else {
2401 let compression_type = sector_data[0];
2403 let compressed_data = §or_data[1..];
2404 match compression::decompress(
2405 compressed_data,
2406 compression_type,
2407 expected_size,
2408 ) {
2409 Ok(decompressed) => decompressed,
2410 Err(e) => {
2411 log::warn!("Failed to decompress sector {i}: {e}. Using zeros.");
2412 vec![0u8; expected_size]
2413 }
2414 }
2415 }
2416 } else {
2417 log::warn!("Empty compressed sector data for sector {i}. Using zeros.");
2418 vec![0u8; expected_size]
2419 }
2420 } else {
2421 sector_data[..expected_size.min(sector_data.len())].to_vec()
2423 };
2424
2425 decompressed_data.extend_from_slice(&decompressed_sector);
2426 }
2427
2428 Ok(decompressed_data)
2429 }
2430
2431 pub fn load_attributes(&mut self) -> Result<()> {
2433 if self.attributes.is_some() {
2435 return Ok(());
2436 }
2437
2438 match self.read_file("(attributes)") {
2440 Ok(mut data) => {
2441 let total_files = if let Some(ref block_table) = self.block_table {
2445 block_table.entries().len()
2446 } else if let Some(ref bet_table) = self.bet_table {
2447 bet_table.header.file_count as usize
2448 } else {
2449 return Err(Error::invalid_format(
2450 "No block/BET table available for attributes",
2451 ));
2452 };
2453
2454 let block_count = {
2457 let flags_from_data = if data.len() >= 8 {
2459 u32::from_le_bytes([data[4], data[5], data[6], data[7]])
2460 } else {
2461 0
2462 };
2463
2464 let mut expected_size_full = 8; if flags_from_data & 0x01 != 0 {
2466 expected_size_full += total_files * 4;
2467 } if flags_from_data & 0x02 != 0 {
2469 expected_size_full += total_files * 8;
2470 } if flags_from_data & 0x04 != 0 {
2472 expected_size_full += total_files * 16;
2473 } if flags_from_data & 0x08 != 0 {
2475 expected_size_full += total_files.div_ceil(8);
2476 } if data.len() == expected_size_full {
2479 log::debug!(
2481 "Attributes file contains entries for all {total_files} files (including itself)"
2482 );
2483 total_files
2484 } else {
2485 let count_minus_1 = total_files.saturating_sub(1);
2487 let mut expected_size_minus1 = 8; if flags_from_data & 0x01 != 0 {
2489 expected_size_minus1 += count_minus_1 * 4;
2490 }
2491 if flags_from_data & 0x02 != 0 {
2492 expected_size_minus1 += count_minus_1 * 8;
2493 }
2494 if flags_from_data & 0x04 != 0 {
2495 expected_size_minus1 += count_minus_1 * 16;
2496 }
2497 if flags_from_data & 0x08 != 0 {
2498 expected_size_minus1 += count_minus_1.div_ceil(8);
2499 }
2500
2501 if data.len() == expected_size_minus1 {
2502 log::debug!(
2503 "Attributes file contains entries for {count_minus_1} files (excluding itself)"
2504 );
2505 count_minus_1
2506 } else {
2507 log::debug!(
2509 "Attributes file size doesn't match expected patterns, using full count {total_files} (actual: {}, expected_full: {expected_size_full}, expected_minus1: {expected_size_minus1})",
2510 data.len()
2511 );
2512 total_files
2513 }
2514 }
2515 };
2516
2517 if data.len() >= 4 {
2520 let first_dword = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
2521
2522 if first_dword != 100 && data[0] != 0x64 {
2524 log::debug!(
2525 "Attributes file may be compressed, first dword: 0x{:08X} ({}), first byte: 0x{:02X}",
2526 first_dword,
2527 first_dword,
2528 data[0]
2529 );
2530
2531 if data[0] & 0x0F != 0 || data[0] == 0x02 {
2533 log::info!(
2534 "Attempting to decompress attributes file with method 0x{:02X}",
2535 data[0]
2536 );
2537 match compression::decompress(&data[1..], data[0], block_count * 100) {
2538 Ok(decompressed) => {
2539 log::info!("Successfully decompressed attributes file");
2540 data = decompressed;
2541 }
2542 Err(e) => {
2543 log::warn!("Failed to decompress attributes file: {e}");
2544 }
2546 }
2547 }
2548 }
2549 }
2550
2551 let attributes = special_files::Attributes::parse(&data.into(), block_count)?;
2553 self.attributes = Some(attributes);
2554
2555 log::info!("Loaded (attributes) file with {block_count} entries");
2556 Ok(())
2557 }
2558 Err(Error::FileNotFound(_)) => {
2559 log::debug!("No (attributes) file found in archive");
2560 Ok(())
2561 }
2562 Err(e) => Err(e),
2563 }
2564 }
2565
2566 pub fn get_file_attributes(
2568 &self,
2569 block_index: usize,
2570 ) -> Option<&special_files::FileAttributes> {
2571 self.attributes.as_ref()?.get_file_attributes(block_index)
2572 }
2573
2574 pub fn attributes(&self) -> Option<&special_files::Attributes> {
2576 self.attributes.as_ref()
2577 }
2578
2579 pub fn add_file(&mut self, _name: &str, _data: &[u8]) -> Result<()> {
2581 Err(Error::invalid_format(
2582 "In-place file addition not yet implemented. Use ArchiveBuilder to create new archives.",
2583 ))
2584 }
2585
2586 fn read_het_table_size(&mut self, het_pos: u64) -> Result<u64> {
2588 log::debug!("Determining HET table size from file structure");
2590
2591 let actual_size = if let Some(bet_pos) = self.header.bet_table_pos {
2593 if bet_pos > het_pos {
2594 bet_pos - het_pos
2596 } else {
2597 self.header.get_hash_table_pos() - het_pos
2599 }
2600 } else {
2601 self.header.get_hash_table_pos() - het_pos
2603 };
2604
2605 log::debug!("HET table position: 0x{het_pos:X}, calculated size: {actual_size} bytes");
2606
2607 Ok(actual_size)
2608 }
2609
2610 fn read_bet_table_size(&mut self, bet_pos: u64) -> Result<u64> {
2612 log::debug!("Determining BET table size from file structure");
2614
2615 let actual_size = self.header.get_hash_table_pos() - bet_pos;
2617
2618 log::debug!("BET table position: 0x{bet_pos:X}, calculated size: {actual_size} bytes");
2619
2620 Ok(actual_size)
2621 }
2622
2623 pub fn verify_signature(&mut self) -> Result<SignatureStatus> {
2625 if let Ok(strong_status) = self.verify_strong_signature()
2627 && strong_status != SignatureStatus::None
2628 {
2629 return Ok(strong_status);
2630 }
2631
2632 self.verify_weak_signature()
2634 }
2635
2636 fn verify_weak_signature(&mut self) -> Result<SignatureStatus> {
2638 let signature_info = match self.find_file("(signature)")? {
2640 Some(info) => info,
2641 None => return Ok(SignatureStatus::None),
2642 };
2643
2644 let signature_data = self.read_file("(signature)")?;
2646
2647 match crate::crypto::parse_weak_signature(&signature_data) {
2649 Ok(weak_sig) => {
2650 let archive_size = self.header.archive_size as u64;
2652 let sig_info = crate::crypto::SignatureInfo::new_weak(
2653 self.archive_offset,
2654 archive_size,
2655 signature_info.file_pos,
2656 signature_info.compressed_size,
2657 weak_sig.clone(),
2658 );
2659
2660 self.reader.seek(SeekFrom::Start(self.archive_offset))?;
2662
2663 match crate::crypto::verify_weak_signature_stormlib(
2665 &mut self.reader,
2666 &weak_sig,
2667 &sig_info,
2668 ) {
2669 Ok(true) => Ok(SignatureStatus::WeakValid),
2670 Ok(false) => Ok(SignatureStatus::WeakInvalid),
2671 Err(e) => {
2672 log::warn!("Failed to verify weak signature: {e}");
2673 Ok(SignatureStatus::WeakInvalid)
2674 }
2675 }
2676 }
2677 Err(_) => {
2678 log::debug!("Signature file found but not a valid weak signature format");
2680 Ok(SignatureStatus::None)
2681 }
2682 }
2683 }
2684
2685 fn read_compressed_table(
2692 &mut self,
2693 offset: u64,
2694 compressed_size: u64,
2695 uncompressed_size: usize,
2696 ) -> Result<Vec<u8>> {
2697 self.reader.seek(SeekFrom::Start(offset))?;
2699
2700 let mut compressed_data = vec![0u8; compressed_size as usize];
2702 self.reader.read_exact(&mut compressed_data)?;
2703
2704 let expected_uncompressed_size = uncompressed_size;
2707
2708 if (compressed_size as usize) < expected_uncompressed_size {
2709 log::debug!(
2711 "Table is compressed: compressed_size={compressed_size}, uncompressed_size={expected_uncompressed_size}"
2712 );
2713
2714 if compressed_data.is_empty() {
2716 return Err(Error::invalid_format("Empty compressed table data"));
2717 }
2718
2719 let compression_type = compressed_data[0];
2720 let compressed_content = &compressed_data[1..];
2721
2722 log::debug!("Decompressing table with method 0x{compression_type:02X}");
2723
2724 compression::decompress(
2726 compressed_content,
2727 compression_type,
2728 expected_uncompressed_size,
2729 )
2730 } else {
2731 log::debug!("Table is not compressed, using as-is");
2733 Ok(compressed_data[..expected_uncompressed_size].to_vec())
2734 }
2735 }
2736
2737 fn verify_strong_signature(&mut self) -> Result<SignatureStatus> {
2739 use crate::crypto::{
2740 STRONG_SIGNATURE_SIZE, parse_strong_signature, verify_strong_signature,
2741 };
2742
2743 let file_size = self.reader.get_ref().metadata()?.len();
2745
2746 let archive_end = self.archive_offset + self.header.get_archive_size();
2748
2749 if file_size < archive_end + STRONG_SIGNATURE_SIZE as u64 {
2751 log::debug!("File too small for strong signature");
2752 return Ok(SignatureStatus::None);
2753 }
2754
2755 let signature_pos = archive_end;
2757 self.reader.seek(SeekFrom::Start(signature_pos))?;
2758
2759 let mut signature_data = vec![0u8; STRONG_SIGNATURE_SIZE];
2761 match self.reader.read_exact(&mut signature_data) {
2762 Ok(()) => {
2763 match parse_strong_signature(&signature_data) {
2765 Ok(strong_sig) => {
2766 log::debug!("Found strong signature at offset 0x{signature_pos:X}");
2767
2768 self.reader.seek(SeekFrom::Start(self.archive_offset))?;
2770
2771 match verify_strong_signature(
2773 &mut self.reader,
2774 &strong_sig,
2775 archive_end - self.archive_offset,
2776 ) {
2777 Ok(true) => {
2778 log::info!("Strong signature verification successful");
2779 Ok(SignatureStatus::StrongValid)
2780 }
2781 Ok(false) => {
2782 log::warn!("Strong signature verification failed");
2783 Ok(SignatureStatus::StrongInvalid)
2784 }
2785 Err(e) => {
2786 log::warn!("Failed to verify strong signature: {e}");
2787 Ok(SignatureStatus::StrongInvalid)
2788 }
2789 }
2790 }
2791 Err(_) => {
2792 log::debug!("No valid strong signature found");
2794 Ok(SignatureStatus::None)
2795 }
2796 }
2797 }
2798 Err(e) => {
2799 log::debug!("Failed to read potential strong signature: {e}");
2800 Ok(SignatureStatus::None)
2801 }
2802 }
2803 }
2804}
2805
2806pub fn decrypt_file_data(data: &mut [u8], key: u32) {
2808 if data.is_empty() || key == 0 {
2809 return;
2810 }
2811
2812 let chunks = data.len() / 4;
2814 if chunks > 0 {
2815 let mut u32_data = Vec::with_capacity(chunks);
2817
2818 for i in 0..chunks {
2820 let offset = i * 4;
2821 let value = u32::from_le_bytes([
2822 data[offset],
2823 data[offset + 1],
2824 data[offset + 2],
2825 data[offset + 3],
2826 ]);
2827 u32_data.push(value);
2828 }
2829
2830 decrypt_block(&mut u32_data, key);
2832
2833 for (i, &value) in u32_data.iter().enumerate() {
2835 let offset = i * 4;
2836 let bytes = value.to_le_bytes();
2837 data[offset] = bytes[0];
2838 data[offset + 1] = bytes[1];
2839 data[offset + 2] = bytes[2];
2840 data[offset + 3] = bytes[3];
2841 }
2842 }
2843
2844 let remainder = data.len() % 4;
2846 if remainder > 0 {
2847 let offset = chunks * 4;
2848
2849 let mut last_bytes = [0u8; 4];
2851 last_bytes[..remainder].copy_from_slice(&data[offset..(remainder + offset)]);
2852 let last_dword = u32::from_le_bytes(last_bytes);
2853
2854 let decrypted = decrypt_dword(last_dword, key.wrapping_add(chunks as u32));
2856
2857 let decrypted_bytes = decrypted.to_le_bytes();
2859 data[offset..(remainder + offset)].copy_from_slice(&decrypted_bytes[..remainder]);
2860 }
2861}
2862
2863#[derive(Debug)]
2865pub struct FileInfo {
2866 pub filename: String,
2868 pub hash_index: usize,
2870 pub block_index: usize,
2872 pub file_pos: u64,
2874 pub compressed_size: u64,
2876 pub file_size: u64,
2878 pub flags: u32,
2880 pub locale: u16,
2882}
2883
2884impl FileInfo {
2885 pub fn is_compressed(&self) -> bool {
2887 use crate::tables::BlockEntry;
2888 (self.flags & (BlockEntry::FLAG_IMPLODE | BlockEntry::FLAG_COMPRESS)) != 0
2889 }
2890
2891 pub fn is_encrypted(&self) -> bool {
2893 use crate::tables::BlockEntry;
2894 (self.flags & BlockEntry::FLAG_ENCRYPTED) != 0
2895 }
2896
2897 pub fn has_fix_key(&self) -> bool {
2899 use crate::tables::BlockEntry;
2900 (self.flags & BlockEntry::FLAG_FIX_KEY) != 0
2901 }
2902
2903 pub fn is_single_unit(&self) -> bool {
2905 use crate::tables::BlockEntry;
2906 (self.flags & BlockEntry::FLAG_SINGLE_UNIT) != 0
2907 }
2908
2909 pub fn has_sector_crc(&self) -> bool {
2911 use crate::tables::BlockEntry;
2912 (self.flags & BlockEntry::FLAG_SECTOR_CRC) != 0
2913 }
2914
2915 pub fn is_patch_file(&self) -> bool {
2917 use crate::tables::BlockEntry;
2918 (self.flags & BlockEntry::FLAG_PATCH_FILE) != 0
2919 }
2920
2921 pub fn is_implode(&self) -> bool {
2923 use crate::tables::BlockEntry;
2924 (self.flags & BlockEntry::FLAG_IMPLODE) != 0
2925 && (self.flags & BlockEntry::FLAG_COMPRESS) == 0
2926 }
2927
2928 pub fn uses_compression_prefix(&self) -> bool {
2930 use crate::tables::BlockEntry;
2931 (self.flags & BlockEntry::FLAG_COMPRESS) != 0
2932 }
2933
2934 pub fn get_compression_method(&self) -> Option<u8> {
2937 use crate::compression::flags;
2938
2939 if !self.is_compressed() {
2940 return None;
2941 }
2942
2943 let compression_mask = (self.flags & 0x0000FF00) >> 8;
2945
2946 log::debug!(
2947 "Compression method extraction: flags=0x{:08X}, mask=0x{:02X}",
2948 self.flags,
2949 compression_mask
2950 );
2951
2952 match compression_mask {
2954 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), _ => {
2962 log::warn!("Unknown compression method in flags: 0x{compression_mask:02X}");
2963 None
2964 }
2965 }
2966 }
2967}
2968
2969#[derive(Debug)]
2971pub struct FileEntry {
2972 pub name: String,
2974 pub size: u64,
2976 pub compressed_size: u64,
2978 pub flags: u32,
2980 pub hashes: Option<(u32, u32)>,
2982 pub table_indices: Option<(usize, Option<usize>)>,
2985}
2986
2987impl FileEntry {
2988 pub fn is_compressed(&self) -> bool {
2990 use crate::tables::BlockEntry;
2991 (self.flags & (BlockEntry::FLAG_IMPLODE | BlockEntry::FLAG_COMPRESS)) != 0
2992 }
2993
2994 pub fn is_encrypted(&self) -> bool {
2996 use crate::tables::BlockEntry;
2997 (self.flags & BlockEntry::FLAG_ENCRYPTED) != 0
2998 }
2999
3000 pub fn has_fix_key(&self) -> bool {
3002 use crate::tables::BlockEntry;
3003 (self.flags & BlockEntry::FLAG_FIX_KEY) != 0
3004 }
3005
3006 pub fn is_single_unit(&self) -> bool {
3008 use crate::tables::BlockEntry;
3009 (self.flags & BlockEntry::FLAG_SINGLE_UNIT) != 0
3010 }
3011
3012 pub fn has_sector_crc(&self) -> bool {
3014 use crate::tables::BlockEntry;
3015 (self.flags & BlockEntry::FLAG_SECTOR_CRC) != 0
3016 }
3017
3018 pub fn exists(&self) -> bool {
3020 use crate::tables::BlockEntry;
3021 (self.flags & BlockEntry::FLAG_EXISTS) != 0
3022 }
3023
3024 pub fn is_patch_file(&self) -> bool {
3026 use crate::tables::BlockEntry;
3027 (self.flags & BlockEntry::FLAG_PATCH_FILE) != 0
3028 }
3029}
3030
3031#[cfg(test)]
3032mod tests {
3033 use super::*;
3034 use crate::encrypt_block;
3035
3036 #[test]
3037 fn test_open_options() {
3038 let opts = OpenOptions::new().load_tables(false);
3039
3040 assert!(!opts.load_tables);
3041 }
3042
3043 #[test]
3044 fn test_file_info_flags() {
3045 use crate::tables::BlockEntry;
3046
3047 let info = FileInfo {
3048 filename: "test.txt".to_string(),
3049 hash_index: 0,
3050 block_index: 0,
3051 file_pos: 0,
3052 compressed_size: 100,
3053 file_size: 200,
3054 flags: BlockEntry::FLAG_COMPRESS | BlockEntry::FLAG_ENCRYPTED,
3055 locale: 0,
3056 };
3057
3058 assert!(info.is_compressed());
3059 assert!(info.is_encrypted());
3060 assert!(!info.has_fix_key());
3061 }
3062
3063 #[test]
3064 fn test_decrypt_file_data() {
3065 let mut data = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
3066 let original = data.clone();
3067
3068 fn encrypt_test_data(data: &mut [u8], key: u32) {
3070 if data.is_empty() || key == 0 {
3071 return;
3072 }
3073
3074 let chunks = data.len() / 4;
3076 if chunks > 0 {
3077 let mut u32_data = Vec::with_capacity(chunks);
3078 for i in 0..chunks {
3079 let offset = i * 4;
3080 let value = u32::from_le_bytes([
3081 data[offset],
3082 data[offset + 1],
3083 data[offset + 2],
3084 data[offset + 3],
3085 ]);
3086 u32_data.push(value);
3087 }
3088
3089 encrypt_block(&mut u32_data, key);
3090
3091 for (i, &value) in u32_data.iter().enumerate() {
3092 let offset = i * 4;
3093 let bytes = value.to_le_bytes();
3094 data[offset] = bytes[0];
3095 data[offset + 1] = bytes[1];
3096 data[offset + 2] = bytes[2];
3097 data[offset + 3] = bytes[3];
3098 }
3099 }
3100 }
3101
3102 encrypt_test_data(&mut data, 0xDEADBEEF);
3104 assert_ne!(data, original, "Data should be changed after encryption");
3105
3106 decrypt_file_data(&mut data, 0xDEADBEEF);
3108 assert_eq!(data, original, "Data should be restored after decryption");
3109 }
3110
3111 #[test]
3112 fn test_crc_calculation() {
3113 let test_data = b"Hello, World!";
3116 let crc = adler2::adler32_slice(test_data);
3117
3118 assert_eq!(crc, 0x1F9E046A);
3120 }
3121}