1use crate::tables::{BlockEntry, HashEntry};
7
8#[derive(Debug, Clone)]
10pub struct HexDumpConfig {
11 pub bytes_per_line: usize,
13 pub show_ascii: bool,
15 pub show_offset: bool,
17 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
32pub 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 if config.show_offset {
49 output.push_str(&format!("{offset:08X} "));
50 }
51
52 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(' '); }
58 }
59
60 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 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
96pub 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
106pub 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#[derive(Debug)]
119pub struct TableFormatter {
120 headers: Vec<String>,
121 rows: Vec<Vec<String>>,
122 column_widths: Vec<usize>,
123}
124
125impl TableFormatter {
126 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 pub fn add_row(&mut self, row: Vec<String>) {
140 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 pub fn format(&self) -> String {
151 let mut output = String::new();
152
153 self.write_separator(&mut output);
155 self.write_row(&mut output, &self.headers);
156 self.write_separator(&mut output);
157
158 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#[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 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 pub fn update(&mut self, current: usize) {
223 self.current = current;
224 }
225
226 pub fn increment(&mut self) {
228 self.current += 1;
229 }
230
231 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 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
257pub 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
275pub 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#[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 pub fn new() -> Self {
308 Self {
309 indent: 0,
310 start_time: std::time::Instant::now(),
311 }
312 }
313
314 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 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 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
379pub 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
420pub 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
468pub 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
494pub 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 pub fn is_imploded(&self) -> bool {
551 (self.flags & Self::FLAG_IMPLODE) != 0
552 }
553}
554
555#[derive(Debug)]
557pub struct ArchiveStructureVisualizer {
558 sections: Vec<(u64, u64, String, String)>, }
560
561impl Default for ArchiveStructureVisualizer {
562 fn default() -> Self {
563 Self::new()
564 }
565}
566
567impl ArchiveStructureVisualizer {
568 pub fn new() -> Self {
570 Self {
571 sections: Vec::new(),
572 }
573 }
574
575 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 pub fn visualize(&mut self) -> String {
583 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 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 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 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 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
645pub fn visualize_archive_structure(info: &crate::ArchiveInfo) -> String {
647 let mut viz = ArchiveStructureVisualizer::new();
648
649 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 viz.add_section(info.archive_offset, 32, "MPQ Header", "Main archive header");
661
662 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 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 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 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 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#[derive(Debug)]
727pub struct FileExtractionTracer {
728 file_name: String,
729 steps: Vec<(String, Option<String>)>, start_time: std::time::Instant,
731}
732
733impl FileExtractionTracer {
734 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 pub fn record_step(&mut self, step: &str, details: Option<String>) {
745 self.steps.push((step.to_string(), details));
746
747 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 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#[derive(Debug)]
783pub struct CompressionAnalyzer {
784 results: Vec<CompressionAnalysisResult>,
785}
786
787#[derive(Debug, Clone)]
789pub struct CompressionAnalysisResult {
790 pub file_name: String,
792 pub block_index: usize,
794 pub compression_mask: u8,
796 pub methods: Vec<&'static str>,
798 pub original_size: u64,
800 pub compressed_size: u64,
802 pub ratio: f64,
804}
805
806impl Default for CompressionAnalyzer {
807 fn default() -> Self {
808 Self::new()
809 }
810}
811
812impl CompressionAnalyzer {
813 pub fn new() -> Self {
815 Self {
816 results: Vec::new(),
817 }
818 }
819
820 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 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 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 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 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 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
951pub fn format_het_table(het: &crate::tables::HetTable) -> String {
953 let mut output = String::new();
954
955 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 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
979pub fn format_bet_table(bet: &crate::tables::BetTable) -> String {
981 let mut output = String::new();
982
983 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 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}