wow_mpq/
debug.rs

1//! Debug utilities for inspecting MPQ archive internals.
2//!
3//! This module provides various debugging tools for analyzing MPQ archives,
4//! including hex dumps, table formatters, and structure visualizers.
5
6use crate::tables::{BlockEntry, HashEntry};
7
8/// Hex dump configuration options
9#[derive(Debug, Clone)]
10pub struct HexDumpConfig {
11    /// Number of bytes per line
12    pub bytes_per_line: usize,
13    /// Show ASCII representation
14    pub show_ascii: bool,
15    /// Show byte offsets
16    pub show_offset: bool,
17    /// Maximum bytes to dump (0 = unlimited)
18    pub max_bytes: usize,
19}
20
21impl Default for HexDumpConfig {
22    fn default() -> Self {
23        Self {
24            bytes_per_line: 16,
25            show_ascii: true,
26            show_offset: true,
27            max_bytes: 512,
28        }
29    }
30}
31
32/// Generate a hex dump of binary data
33pub fn hex_dump(data: &[u8], config: &HexDumpConfig) -> String {
34    let mut output = String::new();
35    let mut offset = 0;
36
37    let max_bytes = if config.max_bytes == 0 {
38        data.len()
39    } else {
40        data.len().min(config.max_bytes)
41    };
42
43    while offset < max_bytes {
44        let chunk_end = (offset + config.bytes_per_line).min(max_bytes);
45        let chunk = &data[offset..chunk_end];
46
47        // Offset
48        if config.show_offset {
49            output.push_str(&format!("{offset:08X}  "));
50        }
51
52        // Hex bytes
53        for (i, byte) in chunk.iter().enumerate() {
54            output.push_str(&format!("{byte:02X} "));
55            if i == 7 && config.bytes_per_line > 8 {
56                output.push(' '); // Extra space in the middle
57            }
58        }
59
60        // Padding for incomplete lines
61        if chunk.len() < config.bytes_per_line {
62            let padding = config.bytes_per_line - chunk.len();
63            for _ in 0..padding {
64                output.push_str("   ");
65            }
66            if config.bytes_per_line > 8 && chunk.len() <= 8 {
67                output.push(' ');
68            }
69        }
70
71        // ASCII representation
72        if config.show_ascii {
73            output.push_str(" |");
74            for byte in chunk {
75                let ch = if *byte >= 0x20 && *byte < 0x7F {
76                    *byte as char
77                } else {
78                    '.'
79                };
80                output.push(ch);
81            }
82            output.push('|');
83        }
84
85        output.push('\n');
86        offset += config.bytes_per_line;
87    }
88
89    if max_bytes < data.len() {
90        output.push_str(&format!("... ({} more bytes)\n", data.len() - max_bytes));
91    }
92
93    output
94}
95
96/// Generate a hex dump with custom configuration
97pub fn hex_dump_custom(data: &[u8], bytes_per_line: usize, max_bytes: usize) -> String {
98    let config = HexDumpConfig {
99        bytes_per_line,
100        max_bytes,
101        ..Default::default()
102    };
103    hex_dump(data, &config)
104}
105
106/// Format a single hex line for inline display
107pub fn hex_string(data: &[u8], max_len: usize) -> String {
108    let len = data.len().min(max_len);
109    let hex: Vec<String> = data[..len].iter().map(|b| format!("{b:02X}")).collect();
110    if data.len() > max_len {
111        format!("{} ... ({} bytes total)", hex.join(" "), data.len())
112    } else {
113        hex.join(" ")
114    }
115}
116
117/// Table formatter for displaying structured data
118#[derive(Debug)]
119pub struct TableFormatter {
120    headers: Vec<String>,
121    rows: Vec<Vec<String>>,
122    column_widths: Vec<usize>,
123}
124
125impl TableFormatter {
126    /// Create a new table formatter with headers
127    pub fn new(headers: Vec<&str>) -> Self {
128        let headers: Vec<String> = headers.into_iter().map(String::from).collect();
129        let column_widths = headers.iter().map(|h| h.len()).collect();
130
131        Self {
132            headers,
133            rows: Vec::new(),
134            column_widths,
135        }
136    }
137
138    /// Add a row to the table
139    pub fn add_row(&mut self, row: Vec<String>) {
140        // Update column widths
141        for (i, cell) in row.iter().enumerate() {
142            if i < self.column_widths.len() {
143                self.column_widths[i] = self.column_widths[i].max(cell.len());
144            }
145        }
146        self.rows.push(row);
147    }
148
149    /// Format the table as a string
150    pub fn format(&self) -> String {
151        let mut output = String::new();
152
153        // Header
154        self.write_separator(&mut output);
155        self.write_row(&mut output, &self.headers);
156        self.write_separator(&mut output);
157
158        // Rows
159        for row in &self.rows {
160            self.write_row(&mut output, row);
161        }
162
163        if !self.rows.is_empty() {
164            self.write_separator(&mut output);
165        }
166
167        output
168    }
169
170    fn write_separator(&self, output: &mut String) {
171        output.push('+');
172        for width in &self.column_widths {
173            output.push('-');
174            for _ in 0..*width {
175                output.push('-');
176            }
177            output.push('-');
178            output.push('+');
179        }
180        output.push('\n');
181    }
182
183    fn write_row(&self, output: &mut String, row: &[String]) {
184        output.push('|');
185        for (i, cell) in row.iter().enumerate() {
186            if i < self.column_widths.len() {
187                output.push(' ');
188                output.push_str(cell);
189                let padding = self.column_widths[i] - cell.len();
190                for _ in 0..padding {
191                    output.push(' ');
192                }
193                output.push(' ');
194            }
195            output.push('|');
196        }
197        output.push('\n');
198    }
199}
200
201/// Progress indicator for long operations
202#[derive(Debug)]
203pub struct ProgressTracker {
204    name: String,
205    total: usize,
206    current: usize,
207    start_time: std::time::Instant,
208}
209
210impl ProgressTracker {
211    /// Create a new progress tracker
212    pub fn new(name: &str, total: usize) -> Self {
213        Self {
214            name: name.to_string(),
215            total,
216            current: 0,
217            start_time: std::time::Instant::now(),
218        }
219    }
220
221    /// Update the current progress
222    pub fn update(&mut self, current: usize) {
223        self.current = current;
224    }
225
226    /// Increment the current progress by 1
227    pub fn increment(&mut self) {
228        self.current += 1;
229    }
230
231    /// Mark the operation as finished and log the total time
232    pub fn finish(&self) {
233        let elapsed = self.start_time.elapsed();
234        log::debug!(
235            "{} completed: {} items in {:.2}s",
236            self.name,
237            self.total,
238            elapsed.as_secs_f64()
239        );
240    }
241
242    /// Log the current progress percentage
243    pub fn log_progress(&self) {
244        if self.total > 0 {
245            let percent = (self.current as f64 / self.total as f64) * 100.0;
246            log::trace!(
247                "{}: {}/{} ({:.1}%)",
248                self.name,
249                self.current,
250                self.total,
251                percent
252            );
253        }
254    }
255}
256
257/// Format file size in human-readable format
258pub fn format_size(bytes: u64) -> String {
259    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
260    let mut size = bytes as f64;
261    let mut unit_index = 0;
262
263    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
264        size /= 1024.0;
265        unit_index += 1;
266    }
267
268    if unit_index == 0 {
269        format!("{} {}", bytes, UNITS[unit_index])
270    } else {
271        format!("{:.2} {}", size, UNITS[unit_index])
272    }
273}
274
275/// Format a bitflag value showing individual flags
276pub fn format_flags(value: u32, flag_names: &[(u32, &str)]) -> String {
277    let mut flags = Vec::new();
278
279    for (flag, name) in flag_names {
280        if value & flag != 0 {
281            flags.push(*name);
282        }
283    }
284
285    if flags.is_empty() {
286        format!("0x{value:08X} (none)")
287    } else {
288        format!("0x{:08X} ({})", value, flags.join(" | "))
289    }
290}
291
292/// Debug context for tracking operation flow
293#[derive(Debug)]
294pub struct DebugContext {
295    indent: usize,
296    start_time: std::time::Instant,
297}
298
299impl Default for DebugContext {
300    fn default() -> Self {
301        Self::new()
302    }
303}
304
305impl DebugContext {
306    /// Create a new debug context
307    pub fn new() -> Self {
308        Self {
309            indent: 0,
310            start_time: std::time::Instant::now(),
311        }
312    }
313
314    /// Enter a new scope, increasing indentation
315    pub fn enter_scope(&mut self, name: &str) {
316        let indent = "  ".repeat(self.indent);
317        log::trace!("{indent}→ {name}");
318        self.indent += 1;
319    }
320
321    /// Exit the current scope, decreasing indentation
322    pub fn exit_scope(&mut self, name: &str) {
323        self.indent = self.indent.saturating_sub(1);
324        let indent = "  ".repeat(self.indent);
325        log::trace!("{}← {} ({}ms)", indent, name, self.elapsed_ms());
326    }
327
328    /// Log a message at the current indentation level
329    pub fn log(&self, message: &str) {
330        let indent = "  ".repeat(self.indent);
331        log::trace!("{indent}  {message}");
332    }
333
334    fn elapsed_ms(&self) -> u64 {
335        self.start_time.elapsed().as_millis() as u64
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_hex_dump() {
345        let data = b"Hello, World!\x00\x01\x02\x03";
346        let dump = hex_dump(data, &HexDumpConfig::default());
347        assert!(dump.contains("48 65 6C 6C 6F 2C 20 57"));
348        assert!(dump.contains("|Hello, World!"));
349    }
350
351    #[test]
352    fn test_table_formatter() {
353        let mut table = TableFormatter::new(vec!["ID", "Name", "Size"]);
354        table.add_row(vec![
355            "1".to_string(),
356            "test.txt".to_string(),
357            "1024".to_string(),
358        ]);
359        table.add_row(vec![
360            "2".to_string(),
361            "data.bin".to_string(),
362            "2048".to_string(),
363        ]);
364
365        let output = table.format();
366        assert!(output.contains("test.txt"));
367        assert!(output.contains("1024"));
368    }
369
370    #[test]
371    fn test_format_size() {
372        assert_eq!(format_size(512), "512 B");
373        assert_eq!(format_size(1024), "1.00 KB");
374        assert_eq!(format_size(1536), "1.50 KB");
375        assert_eq!(format_size(1048576), "1.00 MB");
376    }
377}
378
379/// Format a hash table for display
380pub fn format_hash_table(entries: &[HashEntry]) -> String {
381    let mut table = TableFormatter::new(vec![
382        "Index",
383        "Name1",
384        "Name2",
385        "Locale",
386        "Platform",
387        "Block Index",
388        "Status",
389    ]);
390
391    for (index, entry) in entries.iter().enumerate() {
392        let status = if entry.is_empty() {
393            "Empty"
394        } else if entry.is_deleted() {
395            "Deleted"
396        } else {
397            "Active"
398        };
399
400        table.add_row(vec![
401            format!("{}", index),
402            format!("0x{:08X}", entry.name_1),
403            format!("0x{:08X}", entry.name_2),
404            format!("0x{:04X}", entry.locale),
405            format!("{}", entry.platform),
406            if entry.block_index == HashEntry::EMPTY_NEVER_USED {
407                "FFFFFFFF".to_string()
408            } else if entry.block_index == HashEntry::EMPTY_DELETED {
409                "FFFFFFFE".to_string()
410            } else {
411                format!("{}", entry.block_index)
412            },
413            status.to_string(),
414        ]);
415    }
416
417    table.format()
418}
419
420/// Format a block table for display
421pub fn format_block_table(entries: &[BlockEntry]) -> String {
422    let mut table = TableFormatter::new(vec![
423        "Index",
424        "File Pos",
425        "Comp Size",
426        "File Size",
427        "Flags",
428        "Compression",
429    ]);
430
431    for (index, entry) in entries.iter().enumerate() {
432        let compression = if entry.is_compressed() {
433            if entry.is_imploded() {
434                "PKWARE"
435            } else {
436                "Multi"
437            }
438        } else {
439            "None"
440        };
441
442        table.add_row(vec![
443            format!("{}", index),
444            format!("0x{:08X}", entry.file_pos),
445            format_size(entry.compressed_size as u64),
446            format_size(entry.file_size as u64),
447            format_flags(
448                entry.flags,
449                &[
450                    (BlockEntry::FLAG_IMPLODE, "IMPLODE"),
451                    (BlockEntry::FLAG_COMPRESS, "COMPRESS"),
452                    (BlockEntry::FLAG_ENCRYPTED, "ENCRYPTED"),
453                    (BlockEntry::FLAG_FIX_KEY, "FIX_KEY"),
454                    (BlockEntry::FLAG_PATCH_FILE, "PATCH"),
455                    (BlockEntry::FLAG_SINGLE_UNIT, "SINGLE"),
456                    (BlockEntry::FLAG_DELETE_MARKER, "DELETE"),
457                    (BlockEntry::FLAG_SECTOR_CRC, "CRC"),
458                    (BlockEntry::FLAG_EXISTS, "EXISTS"),
459                ],
460            ),
461            compression.to_string(),
462        ]);
463    }
464
465    table.format()
466}
467
468/// Debug dump for a single hash entry
469pub fn dump_hash_entry(entry: &HashEntry, index: usize) -> String {
470    format!(
471        "HashEntry[{}]:\n  Name1: 0x{:08X}\n  Name2: 0x{:08X}\n  Locale: 0x{:04X}\n  Platform: {}\n  Block Index: {}\n  Status: {}",
472        index,
473        entry.name_1,
474        entry.name_2,
475        entry.locale,
476        entry.platform,
477        if entry.block_index == HashEntry::EMPTY_NEVER_USED {
478            "FFFFFFFF (Never Used)".to_string()
479        } else if entry.block_index == HashEntry::EMPTY_DELETED {
480            "FFFFFFFE (Deleted)".to_string()
481        } else {
482            format!("{}", entry.block_index)
483        },
484        if entry.is_empty() {
485            "Empty"
486        } else if entry.is_deleted() {
487            "Deleted"
488        } else {
489            "Active"
490        }
491    )
492}
493
494/// Debug dump for a single block entry
495pub fn dump_block_entry(entry: &BlockEntry, index: usize) -> String {
496    let mut flags_str = Vec::new();
497
498    if entry.flags & BlockEntry::FLAG_IMPLODE != 0 {
499        flags_str.push("IMPLODE");
500    }
501    if entry.flags & BlockEntry::FLAG_COMPRESS != 0 {
502        flags_str.push("COMPRESS");
503    }
504    if entry.flags & BlockEntry::FLAG_ENCRYPTED != 0 {
505        flags_str.push("ENCRYPTED");
506    }
507    if entry.flags & BlockEntry::FLAG_FIX_KEY != 0 {
508        flags_str.push("FIX_KEY");
509    }
510    if entry.flags & BlockEntry::FLAG_PATCH_FILE != 0 {
511        flags_str.push("PATCH");
512    }
513    if entry.flags & BlockEntry::FLAG_SINGLE_UNIT != 0 {
514        flags_str.push("SINGLE_UNIT");
515    }
516    if entry.flags & BlockEntry::FLAG_DELETE_MARKER != 0 {
517        flags_str.push("DELETE_MARKER");
518    }
519    if entry.flags & BlockEntry::FLAG_SECTOR_CRC != 0 {
520        flags_str.push("SECTOR_CRC");
521    }
522    if entry.flags & BlockEntry::FLAG_EXISTS != 0 {
523        flags_str.push("EXISTS");
524    }
525
526    format!(
527        "BlockEntry[{}]:\n  File Position: 0x{:08X}\n  Compressed Size: {} ({})\n  File Size: {} ({})\n  Flags: 0x{:08X} [{}]\n  Compression: {}",
528        index,
529        entry.file_pos,
530        entry.compressed_size,
531        format_size(entry.compressed_size as u64),
532        entry.file_size,
533        format_size(entry.file_size as u64),
534        entry.flags,
535        flags_str.join(", "),
536        if entry.is_compressed() {
537            if entry.is_imploded() {
538                "PKWARE Implode"
539            } else {
540                "Multiple Methods"
541            }
542        } else {
543            "None"
544        }
545    )
546}
547
548impl BlockEntry {
549    /// Check if file uses PKWARE implode compression
550    pub fn is_imploded(&self) -> bool {
551        (self.flags & Self::FLAG_IMPLODE) != 0
552    }
553}
554
555/// Archive structure visualization
556#[derive(Debug)]
557pub struct ArchiveStructureVisualizer {
558    sections: Vec<(u64, u64, String, String)>, // (offset, size, name, description)
559}
560
561impl Default for ArchiveStructureVisualizer {
562    fn default() -> Self {
563        Self::new()
564    }
565}
566
567impl ArchiveStructureVisualizer {
568    /// Create a new archive structure visualizer
569    pub fn new() -> Self {
570        Self {
571            sections: Vec::new(),
572        }
573    }
574
575    /// Add a section to the visualization
576    pub fn add_section(&mut self, offset: u64, size: u64, name: &str, description: &str) {
577        self.sections
578            .push((offset, size, name.to_string(), description.to_string()));
579    }
580
581    /// Generate the visual representation
582    pub fn visualize(&mut self) -> String {
583        // Sort sections by offset
584        self.sections.sort_by_key(|s| s.0);
585
586        let mut output = String::new();
587        output.push_str("MPQ Archive Structure:\n");
588        output.push_str("=====================\n\n");
589
590        let max_name_len = self.sections.iter().map(|s| s.2.len()).max().unwrap_or(10);
591
592        // Header
593        output.push_str(&format!(
594            "{:>10} | {:>10} | {:<width$} | Description\n",
595            "Offset",
596            "Size",
597            "Section",
598            width = max_name_len
599        ));
600        output.push_str(&format!(
601            "{:-<10}-+-{:-<10}-+-{:-<width$}-+-{:-<40}\n",
602            "",
603            "",
604            "",
605            "",
606            width = max_name_len
607        ));
608
609        // Sections
610        for (offset, size, name, desc) in &self.sections {
611            output.push_str(&format!(
612                "0x{:08X} | {:>10} | {:<width$} | {}\n",
613                offset,
614                format_size(*size),
615                name,
616                desc,
617                width = max_name_len
618            ));
619        }
620
621        // Visual block diagram
622        output.push_str("\nVisual Layout:\n");
623        output.push_str("-------------\n");
624
625        let mut current_offset = 0u64;
626        for (offset, size, name, _) in &self.sections {
627            // Add gap if there is one
628            if *offset > current_offset {
629                let gap = *offset - current_offset;
630                output.push_str(&format!("│ {:^20} │ {} gap\n", "...", format_size(gap)));
631            }
632
633            output.push_str("├──────────────────────┤\n");
634            output.push_str(&format!("│ {name:^20} │ @ 0x{offset:08X}\n"));
635            output.push_str(&format!("│ {:^20} │ {}\n", format_size(*size), ""));
636
637            current_offset = offset + size;
638        }
639        output.push_str("└──────────────────────┘\n");
640
641        output
642    }
643}
644
645/// Create an archive structure visualization from archive info
646pub fn visualize_archive_structure(info: &crate::ArchiveInfo) -> String {
647    let mut viz = ArchiveStructureVisualizer::new();
648
649    // User data (if present)
650    if let Some(user_data) = &info.user_data_info {
651        viz.add_section(
652            0,
653            user_data.header_size as u64 + user_data.data_size as u64,
654            "User Data",
655            "Custom user data section",
656        );
657    }
658
659    // MPQ Header
660    viz.add_section(info.archive_offset, 32, "MPQ Header", "Main archive header");
661
662    // Hash Table
663    if let Some(size) = info.hash_table_info.size {
664        viz.add_section(
665            info.hash_table_info.offset,
666            info.hash_table_info
667                .compressed_size
668                .unwrap_or(size as u64 * 16),
669            "Hash Table",
670            &format!("{size} entries"),
671        );
672    }
673
674    // Block Table
675    if let Some(size) = info.block_table_info.size {
676        viz.add_section(
677            info.block_table_info.offset,
678            info.block_table_info
679                .compressed_size
680                .unwrap_or(size as u64 * 16),
681            "Block Table",
682            &format!("{size} entries"),
683        );
684    }
685
686    // HET Table (v3+)
687    if let Some(het_info) = &info.het_table_info
688        && let Some(size) = het_info.size
689    {
690        viz.add_section(
691            het_info.offset,
692            het_info.compressed_size.unwrap_or(size as u64),
693            "HET Table",
694            "Extended hash table (v3+)",
695        );
696    }
697
698    // BET Table (v3+)
699    if let Some(bet_info) = &info.bet_table_info
700        && let Some(size) = bet_info.size
701    {
702        viz.add_section(
703            bet_info.offset,
704            bet_info.compressed_size.unwrap_or(size as u64),
705            "BET Table",
706            "Extended block table (v3+)",
707        );
708    }
709
710    // Hi-block table (v2+)
711    if let Some(hi_info) = &info.hi_block_table_info
712        && let Some(size) = hi_info.size
713    {
714        viz.add_section(
715            hi_info.offset,
716            hi_info.compressed_size.unwrap_or(size as u64 * 8),
717            "Hi-Block Table",
718            "High 32-bits of block offsets (v2+)",
719        );
720    }
721
722    viz.visualize()
723}
724
725/// File extraction tracer for detailed debugging
726#[derive(Debug)]
727pub struct FileExtractionTracer {
728    file_name: String,
729    steps: Vec<(String, Option<String>)>, // (step description, optional details)
730    start_time: std::time::Instant,
731}
732
733impl FileExtractionTracer {
734    /// Create a new file extraction tracer
735    pub fn new(file_name: &str) -> Self {
736        Self {
737            file_name: file_name.to_string(),
738            steps: Vec::new(),
739            start_time: std::time::Instant::now(),
740        }
741    }
742
743    /// Record a step in the extraction process
744    pub fn record_step(&mut self, step: &str, details: Option<String>) {
745        self.steps.push((step.to_string(), details));
746
747        // Log immediately if tracing is enabled
748        if log::log_enabled!(log::Level::Trace) {
749            let elapsed = self.start_time.elapsed().as_millis();
750            if let Some(ref details) = self.steps.last().unwrap().1 {
751                log::trace!("[{}ms] {} - {}: {}", elapsed, self.file_name, step, details);
752            } else {
753                log::trace!("[{}ms] {} - {}", elapsed, self.file_name, step);
754            }
755        }
756    }
757
758    /// Generate a detailed report of the extraction process
759    pub fn generate_report(&self) -> String {
760        let mut output = String::new();
761        output.push_str(&format!("File Extraction Trace: {}\n", self.file_name));
762        output.push_str(&format!("{:=<50}\n", ""));
763
764        let total_time = self.start_time.elapsed();
765
766        for (i, (step, details)) in self.steps.iter().enumerate() {
767            output.push_str(&format!("{:2}. {}\n", i + 1, step));
768            if let Some(details) = details {
769                output.push_str(&format!("    └─ {details}\n"));
770            }
771        }
772
773        output.push_str(&format!(
774            "\nTotal extraction time: {:.2}ms\n",
775            total_time.as_secs_f64() * 1000.0
776        ));
777        output
778    }
779}
780
781/// Compression method analyzer
782#[derive(Debug)]
783pub struct CompressionAnalyzer {
784    results: Vec<CompressionAnalysisResult>,
785}
786
787/// Result of compression analysis for a single file
788#[derive(Debug, Clone)]
789pub struct CompressionAnalysisResult {
790    /// Name of the file analyzed
791    pub file_name: String,
792    /// Block index in the archive
793    pub block_index: usize,
794    /// Compression mask byte
795    pub compression_mask: u8,
796    /// List of compression methods used
797    pub methods: Vec<&'static str>,
798    /// Original uncompressed size
799    pub original_size: u64,
800    /// Compressed size in archive
801    pub compressed_size: u64,
802    /// Compression ratio (compressed/original)
803    pub ratio: f64,
804}
805
806impl Default for CompressionAnalyzer {
807    fn default() -> Self {
808        Self::new()
809    }
810}
811
812impl CompressionAnalyzer {
813    /// Create a new compression analyzer
814    pub fn new() -> Self {
815        Self {
816            results: Vec::new(),
817        }
818    }
819
820    /// Analyze compression methods from a mask
821    pub fn analyze_compression_mask(mask: u8) -> Vec<&'static str> {
822        let mut methods = Vec::new();
823
824        if mask & 0x02 != 0 {
825            methods.push("ZLIB");
826        }
827        if mask & 0x08 != 0 {
828            methods.push("PKWARE");
829        }
830        if mask & 0x10 != 0 {
831            methods.push("BZIP2");
832        }
833        if mask & 0x20 != 0 {
834            methods.push("SPARSE");
835        }
836        if mask & 0x40 != 0 {
837            methods.push("ADPCM_MONO");
838        }
839        if mask & 0x80 != 0 {
840            methods.push("ADPCM_STEREO");
841        }
842        if mask & 0x12 != 0 {
843            methods.push("LZMA");
844        }
845
846        if methods.is_empty() {
847            methods.push("NONE");
848        }
849
850        methods
851    }
852
853    /// Add analysis result
854    pub fn add_result(
855        &mut self,
856        file_name: &str,
857        block_index: usize,
858        compression_mask: u8,
859        original_size: u64,
860        compressed_size: u64,
861    ) {
862        let methods = Self::analyze_compression_mask(compression_mask);
863        let ratio = if original_size > 0 {
864            compressed_size as f64 / original_size as f64
865        } else {
866            1.0
867        };
868
869        self.results.push(CompressionAnalysisResult {
870            file_name: file_name.to_string(),
871            block_index,
872            compression_mask,
873            methods,
874            original_size,
875            compressed_size,
876            ratio,
877        });
878    }
879
880    /// Generate compression statistics report
881    pub fn generate_report(&self) -> String {
882        let mut output = String::new();
883        output.push_str("Compression Analysis Report\n");
884        output.push_str("==========================\n\n");
885
886        // Summary statistics
887        let total_original: u64 = self.results.iter().map(|r| r.original_size).sum();
888        let total_compressed: u64 = self.results.iter().map(|r| r.compressed_size).sum();
889        let overall_ratio = if total_original > 0 {
890            total_compressed as f64 / total_original as f64
891        } else {
892            1.0
893        };
894
895        output.push_str(&format!("Total files analyzed: {}\n", self.results.len()));
896        output.push_str(&format!(
897            "Total original size: {}\n",
898            format_size(total_original)
899        ));
900        output.push_str(&format!(
901            "Total compressed size: {}\n",
902            format_size(total_compressed)
903        ));
904        output.push_str(&format!(
905            "Overall compression ratio: {:.1}%\n\n",
906            overall_ratio * 100.0
907        ));
908
909        // Method usage statistics
910        let mut method_counts = std::collections::HashMap::new();
911        for result in &self.results {
912            for method in &result.methods {
913                *method_counts.entry(*method).or_insert(0) += 1;
914            }
915        }
916
917        output.push_str("Compression methods used:\n");
918        for (method, count) in method_counts.iter() {
919            output.push_str(&format!("  {method}: {count} files\n"));
920        }
921
922        // Detailed results table
923        output.push_str("\nDetailed Results:\n");
924        output.push_str("-----------------\n");
925
926        let mut table = TableFormatter::new(vec![
927            "File",
928            "Block",
929            "Methods",
930            "Original",
931            "Compressed",
932            "Ratio",
933        ]);
934
935        for result in &self.results {
936            table.add_row(vec![
937                result.file_name.clone(),
938                format!("{}", result.block_index),
939                result.methods.join(", "),
940                format_size(result.original_size),
941                format_size(result.compressed_size),
942                format!("{:.1}%", result.ratio * 100.0),
943            ]);
944        }
945
946        output.push_str(&table.format());
947        output
948    }
949}
950
951/// Format HET table for display
952pub fn format_het_table(het: &crate::tables::HetTable) -> String {
953    let mut output = String::new();
954
955    // Copy packed struct fields to local variables to avoid alignment issues
956    let table_size = het.header.table_size;
957    let max_file_count = het.header.max_file_count;
958    let hash_table_size = het.header.hash_table_size;
959    let hash_entry_size = het.header.hash_entry_size;
960    let total_index_size = het.header.total_index_size;
961    let index_size_extra = het.header.index_size_extra;
962    let index_size = het.header.index_size;
963    let block_table_size = het.header.block_table_size;
964
965    // Header information
966    output.push_str("HET Table Header:\n");
967    output.push_str(&format!("  Table Size: {table_size} bytes\n"));
968    output.push_str(&format!("  Max File Count: {max_file_count}\n"));
969    output.push_str(&format!("  Hash Table Size: {hash_table_size} bytes\n"));
970    output.push_str(&format!("  Hash Entry Size: {hash_entry_size} bits\n"));
971    output.push_str(&format!("  Total Index Size: {total_index_size} bits\n"));
972    output.push_str(&format!("  Index Size Extra: {index_size_extra} bits\n"));
973    output.push_str(&format!("  Index Size: {index_size} bits\n"));
974    output.push_str(&format!("  Block Table Size: {block_table_size} bytes\n"));
975
976    output
977}
978
979/// Format BET table for display
980pub fn format_bet_table(bet: &crate::tables::BetTable) -> String {
981    let mut output = String::new();
982
983    // Copy packed struct fields to local variables to avoid alignment issues
984    let table_size = bet.header.table_size;
985    let file_count = bet.header.file_count;
986    let unknown_08 = bet.header.unknown_08;
987    let table_entry_size = bet.header.table_entry_size;
988    let bit_index_file_pos = bet.header.bit_index_file_pos;
989    let bit_count_file_pos = bet.header.bit_count_file_pos;
990    let bit_index_file_size = bet.header.bit_index_file_size;
991    let bit_count_file_size = bet.header.bit_count_file_size;
992    let bit_index_cmp_size = bet.header.bit_index_cmp_size;
993    let bit_count_cmp_size = bet.header.bit_count_cmp_size;
994    let bit_index_flag_index = bet.header.bit_index_flag_index;
995    let bit_count_flag_index = bet.header.bit_count_flag_index;
996    let bit_index_unknown = bet.header.bit_index_unknown;
997    let bit_count_unknown = bet.header.bit_count_unknown;
998    let total_bet_hash_size = bet.header.total_bet_hash_size;
999    let bet_hash_size_extra = bet.header.bet_hash_size_extra;
1000    let bet_hash_size = bet.header.bet_hash_size;
1001    let bet_hash_array_size = bet.header.bet_hash_array_size;
1002    let flag_count = bet.header.flag_count;
1003
1004    // Header information
1005    output.push_str("BET Table Header:\n");
1006    output.push_str(&format!("  Table Size: {table_size} bytes\n"));
1007    output.push_str(&format!("  File Count: {file_count}\n"));
1008    output.push_str(&format!("  Unknown: 0x{unknown_08:08X}\n"));
1009    output.push_str(&format!("  Table Entry Size: {table_entry_size} bits\n"));
1010
1011    output.push_str("\nBit Field Positions:\n");
1012    output.push_str(&format!(
1013        "  File Position: bit {bit_index_file_pos} (width: {bit_count_file_pos})\n"
1014    ));
1015    output.push_str(&format!(
1016        "  File Size: bit {bit_index_file_size} (width: {bit_count_file_size})\n"
1017    ));
1018    output.push_str(&format!(
1019        "  Compressed Size: bit {bit_index_cmp_size} (width: {bit_count_cmp_size})\n"
1020    ));
1021    output.push_str(&format!(
1022        "  Flag Index: bit {bit_index_flag_index} (width: {bit_count_flag_index})\n"
1023    ));
1024    output.push_str(&format!(
1025        "  Unknown: bit {bit_index_unknown} (width: {bit_count_unknown})\n"
1026    ));
1027
1028    output.push_str("\nHash Information:\n");
1029    output.push_str(&format!("  Total Hash Size: {total_bet_hash_size} bytes\n"));
1030    output.push_str(&format!(
1031        "  BET Hash Size Extra: {bet_hash_size_extra} bits\n"
1032    ));
1033    output.push_str(&format!("  BET Hash Size: {bet_hash_size} bits\n"));
1034    output.push_str(&format!(
1035        "  BET Hash Array Size: {bet_hash_array_size} bytes\n"
1036    ));
1037    output.push_str(&format!("  Flag Count: {flag_count}\n"));
1038
1039    output
1040}