1use crate::{
2 InspectCommands, OutputFormat,
3 cached_client::create_client,
4 output::{
5 OutputStyle, create_table, format_count_badge, format_header, format_key_value,
6 format_success, format_warning, header_cell, numeric_cell, print_section_header,
7 print_subsection_header, regular_cell,
8 },
9};
10use blte::decompress_blte;
11use ngdp_bpsv::BpsvDocument;
12use ngdp_cache::cached_cdn_client::CachedCdnClient;
13use ngdp_crypto::KeyService;
14use ribbit_client::{Endpoint, ProductCdnsResponse, ProductVersionsResponse, Region};
15use std::str::FromStr;
16use tact_parser::{
17 config::BuildConfig, download::DownloadManifest, encoding::EncodingFile,
18 install::InstallManifest, size::SizeFile,
19};
20
21pub async fn handle(
22 cmd: InspectCommands,
23 format: OutputFormat,
24) -> Result<(), Box<dyn std::error::Error>> {
25 match cmd {
26 InspectCommands::Bpsv { input, raw } => inspect_bpsv(input, raw, format).await?,
27 InspectCommands::BuildConfig {
28 product,
29 build,
30 region,
31 } => {
32 inspect_build_config(product, build, region, format).await?;
33 }
34 InspectCommands::CdnConfig { product, region } => {
35 inspect_cdn_config(product, region, format).await?;
36 }
37 InspectCommands::Encoding {
38 product,
39 region,
40 stats,
41 search,
42 limit,
43 } => {
44 inspect_encoding(product, region, stats, search, limit, format).await?;
45 }
46 InspectCommands::Install {
47 product,
48 region,
49 tags,
50 all,
51 } => {
52 inspect_install(product, region, tags, all, format).await?;
53 }
54 InspectCommands::DownloadManifest {
55 product,
56 region,
57 priority_limit,
58 tags,
59 } => {
60 inspect_download_manifest(product, region, priority_limit, tags, format).await?;
61 }
62 InspectCommands::Size {
63 product,
64 region,
65 largest,
66 tags,
67 } => {
68 inspect_size(product, region, largest, tags, format).await?;
69 }
70 }
71 Ok(())
72}
73
74async fn inspect_bpsv(
75 input: String,
76 raw: bool,
77 format: OutputFormat,
78) -> Result<(), Box<dyn std::error::Error>> {
79 let data = if input.starts_with("http://") || input.starts_with("https://") {
81 let response = reqwest::get(&input).await?;
83 response.text().await?
84 } else {
85 std::fs::read_to_string(&input)?
87 };
88
89 if raw {
90 println!("{data}");
91 return Ok(());
92 }
93
94 let doc = BpsvDocument::parse(&data)?;
95
96 match format {
97 OutputFormat::Json | OutputFormat::JsonPretty => {
98 let json_data = serde_json::json!({
99 "schema": doc.schema().field_names(),
100 "sequence_number": doc.sequence_number(),
101 "row_count": doc.rows().len(),
102 "rows": doc.rows().iter().map(|row| {
103 let mut map = serde_json::Map::new();
104 for (name, value) in doc.schema().field_names().iter().zip(row.raw_values()) {
105 map.insert(name.to_string(), serde_json::Value::String(value.to_string()));
106 }
107 map
108 }).collect::<Vec<_>>()
109 });
110
111 let output = if matches!(format, OutputFormat::JsonPretty) {
112 serde_json::to_string_pretty(&json_data)?
113 } else {
114 serde_json::to_string(&json_data)?
115 };
116 println!("{output}");
117 }
118 OutputFormat::Bpsv => {
119 println!("{}", doc.to_bpsv_string());
120 }
121 OutputFormat::Text => {
122 let style = OutputStyle::new();
123
124 print_section_header("BPSV Document Analysis", &style);
125
126 print_subsection_header("Schema", &style);
127 let mut schema_table = create_table(&style);
128 schema_table.set_header(vec![
129 header_cell("Index", &style),
130 header_cell("Field Name", &style),
131 header_cell("Type", &style),
132 ]);
133
134 for (i, field) in doc.schema().fields().iter().enumerate() {
135 schema_table.add_row(vec![
136 numeric_cell(&i.to_string()),
137 regular_cell(&field.name),
138 regular_cell(&field.field_type.to_string()),
139 ]);
140 }
141 println!("{schema_table}");
142
143 if let Some(seq) = doc.sequence_number() {
144 println!();
145 println!(
146 "{}",
147 format_key_value("Sequence Number", &seq.to_string(), &style)
148 );
149 }
150
151 print_subsection_header(
152 &format!(
153 "Data {}",
154 format_count_badge(doc.rows().len(), "row", &style)
155 ),
156 &style,
157 );
158
159 if !doc.rows().is_empty() {
160 let preview_count = std::cmp::min(5, doc.rows().len());
162 println!(
163 "\n{}",
164 format_header(&format!("Preview (first {preview_count} rows)"), &style)
165 );
166
167 let mut data_table = create_table(&style);
168
169 let mut headers = vec![header_cell("#", &style)];
171 headers.extend(
172 doc.schema()
173 .field_names()
174 .iter()
175 .map(|name| header_cell(name, &style)),
176 );
177 data_table.set_header(headers);
178
179 for (i, row) in doc.rows().iter().take(preview_count).enumerate() {
181 let mut cells = vec![numeric_cell(&(i + 1).to_string())];
182 cells.extend(row.raw_values().iter().map(|v| regular_cell(v)));
183 data_table.add_row(cells);
184 }
185
186 println!("{data_table}");
187
188 if doc.rows().len() > preview_count {
189 println!(
190 "\n{}",
191 format_header(
192 &format!("... and {} more rows", doc.rows().len() - preview_count),
193 &style
194 )
195 );
196 }
197 }
198 }
199 }
200
201 Ok(())
202}
203
204async fn inspect_build_config(
206 product: String,
207 build: String,
208 region: String,
209 format: OutputFormat,
210) -> Result<(), Box<dyn std::error::Error>> {
211 let style = OutputStyle::new();
212 let region_enum = Region::from_str(®ion)?;
213
214 print_section_header(
216 &format!("Build Config Analysis: {product} (Build {build})"),
217 &style,
218 );
219
220 let client = create_client(region_enum).await?;
221 let versions_endpoint = Endpoint::ProductVersions(product.clone());
222 let versions: ProductVersionsResponse = client.request_typed(&versions_endpoint).await?;
223
224 let build_entry = versions
226 .entries
227 .iter()
228 .filter(|e| e.region == region)
229 .find(|e| e.build_id.to_string() == build || e.versions_name == build);
230
231 let build_entry = match build_entry {
232 Some(entry) => entry,
233 None => {
234 eprintln!(
235 "{}",
236 format_warning(
237 &format!("Build '{build}' not found for {product} in region {region}"),
238 &style
239 )
240 );
241 return Ok(());
242 }
243 };
244
245 println!("{}", format_key_value("Product", &product, &style));
246 println!("{}", format_key_value("Region", ®ion, &style));
247 println!(
248 "{}",
249 format_key_value("Build ID", &build_entry.build_id.to_string(), &style)
250 );
251 println!(
252 "{}",
253 format_key_value("Version", &build_entry.versions_name, &style)
254 );
255 println!(
256 "{}",
257 format_key_value("Build Config Hash", &build_entry.build_config, &style)
258 );
259 println!();
260
261 let cdns_endpoint = Endpoint::ProductCdns(product.clone());
263 let cdns: ProductCdnsResponse = client.request_typed(&cdns_endpoint).await?;
264
265 let cdn_entry = cdns.entries.iter().find(|e| e.name == region);
266 let cdn_entry = match cdn_entry {
267 Some(entry) => entry,
268 None => {
269 eprintln!(
270 "{}",
271 format_warning(
272 &format!("No CDN configuration found for region {region}"),
273 &style
274 )
275 );
276 return Ok(());
277 }
278 };
279
280 print_subsection_header("Downloading Build Configuration", &style);
282
283 let cdn_client = CachedCdnClient::new().await?;
284 cdn_client.add_primary_hosts(cdn_entry.hosts.iter().cloned());
286 cdn_client.add_fallback_host("cdn.arctium.tools");
287 cdn_client.add_fallback_host("tact.mirror.reliquaryhq.com");
288 let cdn_host = &cdn_entry.hosts[0]; let cdn_path = &cdn_entry.path;
290
291 println!(
292 "Downloading from: {}/{}/config/{}",
293 cdn_host, cdn_path, &build_entry.build_config
294 );
295
296 let response = cdn_client
297 .download_build_config(cdn_host, cdn_path, &build_entry.build_config)
298 .await?;
299 let config_text = response.text().await?;
300
301 let build_config = BuildConfig::parse(&config_text)?;
303
304 match format {
305 OutputFormat::Json | OutputFormat::JsonPretty => {
306 output_build_config_json(&build_config, format)?;
307 }
308 OutputFormat::Text => {
309 output_build_config_tree(&build_config, &style);
310 }
311 OutputFormat::Bpsv => {
312 println!("{config_text}");
313 }
314 }
315
316 Ok(())
317}
318
319fn output_build_config_tree(config: &BuildConfig, style: &OutputStyle) {
321 print_subsection_header("Build Configuration Tree", style);
322
323 println!("📁 {}", format_header("Core Game Files", style));
325
326 if let Some(root_hash) = config.root_hash() {
327 println!("├── 🗂️ Root File");
328 println!("│ ├── Hash: {root_hash}");
329 if let Some(size) = config.config.get_size("root") {
330 println!(
331 "│ └── Size: {} bytes ({:.2} MB)",
332 size,
333 size as f64 / (1024.0 * 1024.0)
334 );
335 }
336 }
337
338 if let Some(encoding_hash) = config.encoding_hash() {
339 println!("├── 🔗 Encoding File (CKey ↔ EKey mapping)");
340 println!("│ ├── Hash: {encoding_hash}");
341 if let Some(size) = config.config.get_size("encoding") {
342 println!(
343 "│ └── Size: {} bytes ({:.2} KB)",
344 size,
345 size as f64 / 1024.0
346 );
347 }
348 }
349
350 if let Some(install_hash) = config.install_hash() {
351 println!("├── 📦 Install Manifest");
352 println!("│ ├── Hash: {install_hash}");
353 if let Some(size) = config.config.get_size("install") {
354 println!(
355 "│ └── Size: {} bytes ({:.2} KB)",
356 size,
357 size as f64 / 1024.0
358 );
359 }
360 }
361
362 if let Some(download_hash) = config.download_hash() {
363 println!("├── ⬇️ Download Manifest");
364 println!("│ ├── Hash: {download_hash}");
365 if let Some(size) = config.config.get_size("download") {
366 println!(
367 "│ └── Size: {} bytes ({:.2} KB)",
368 size,
369 size as f64 / 1024.0
370 );
371 }
372 }
373
374 if let Some(size_hash) = config.size_hash() {
375 println!("└── 📏 Size File");
376 println!(" ├── Hash: {size_hash}");
377 if let Some(size) = config.config.get_size("size") {
378 println!(
379 " └── Size: {} bytes ({:.2} KB)",
380 size,
381 size as f64 / 1024.0
382 );
383 }
384 }
385
386 println!();
387
388 println!("📋 {}", format_header("Build Information", style));
390
391 if let Some(build_name) = config.build_name() {
392 println!("├── Version: {}", format_success(build_name, style));
393 }
394 if let Some(build_uid) = config.config.get_value("build-uid") {
395 println!("├── Build UID: {build_uid}");
396 }
397 if let Some(build_product) = config.config.get_value("build-product") {
398 println!("├── Product: {build_product}");
399 }
400 if let Some(installer) = config.config.get_value("build-playbuild-installer") {
401 println!("└── Installer: {installer}");
402 }
403
404 println!();
405
406 println!("🔄 {}", format_header("Patching", style));
408
409 let has_patch = config
410 .config
411 .get_value("patch")
412 .is_some_and(|v| !v.is_empty());
413 if has_patch {
414 if let Some(patch_hash) = config.config.get_value("patch") {
415 println!("├── ✅ Patch Available");
416 println!("│ └── Hash: {patch_hash}");
417 }
418 } else {
419 println!("└── ❌ No patch data");
420 }
421
422 println!();
423
424 println!("🗃️ {}", format_header("Virtual File System (VFS)", style));
426
427 let mut vfs_entries = Vec::new();
428 for key in config.config.keys() {
429 if key.starts_with("vfs-") {
430 if let Some(value) = config.config.get_value(key) {
431 vfs_entries.push((key, value));
432 }
433 }
434 }
435
436 if !vfs_entries.is_empty() {
437 vfs_entries.sort_by_key(|(k, _)| *k);
438 for (i, (key, value)) in vfs_entries.iter().enumerate() {
439 let is_last = i == vfs_entries.len() - 1;
440 let prefix = if is_last { "└──" } else { "├──" };
441
442 if value.is_empty() {
443 println!("{} {}: {}", prefix, key, format_warning("(empty)", style));
444 } else {
445 println!("{prefix} {key}: {value}");
446 }
447 }
448 } else {
449 println!("└── No VFS entries found");
450 }
451
452 println!();
453
454 print_subsection_header("Raw Configuration Entries", style);
456
457 let mut table = create_table(style);
458 table.set_header(vec![
459 header_cell("Key", style),
460 header_cell("Value", style),
461 header_cell("Type", style),
462 ]);
463
464 let mut keys: Vec<_> = config.config.keys().into_iter().collect();
465 keys.sort();
466
467 for key in keys {
468 if let Some(value) = config.config.get_value(key) {
469 let value_type = if config.config.get_hash(key).is_some() {
470 "Hash + Size"
471 } else if value.is_empty() {
472 "Empty"
473 } else if value.chars().all(|c| c.is_ascii_digit()) {
474 "Number"
475 } else {
476 "String"
477 };
478
479 let display_value = if value.len() > 50 {
480 format!("{}...", &value[..47])
481 } else {
482 value.to_string()
483 };
484
485 table.add_row(vec![
486 regular_cell(key),
487 regular_cell(&display_value),
488 regular_cell(value_type),
489 ]);
490 }
491 }
492
493 println!("{table}");
494}
495
496fn output_build_config_json(
498 config: &BuildConfig,
499 format: OutputFormat,
500) -> Result<(), Box<dyn std::error::Error>> {
501 let mut json_data = serde_json::Map::new();
502
503 if let Some(hash) = config.root_hash() {
505 json_data.insert(
506 "root_hash".to_string(),
507 serde_json::Value::String(hash.to_string()),
508 );
509 }
510 if let Some(hash) = config.encoding_hash() {
511 json_data.insert(
512 "encoding_hash".to_string(),
513 serde_json::Value::String(hash.to_string()),
514 );
515 }
516 if let Some(hash) = config.install_hash() {
517 json_data.insert(
518 "install_hash".to_string(),
519 serde_json::Value::String(hash.to_string()),
520 );
521 }
522 if let Some(hash) = config.download_hash() {
523 json_data.insert(
524 "download_hash".to_string(),
525 serde_json::Value::String(hash.to_string()),
526 );
527 }
528 if let Some(hash) = config.size_hash() {
529 json_data.insert(
530 "size_hash".to_string(),
531 serde_json::Value::String(hash.to_string()),
532 );
533 }
534
535 if let Some(name) = config.build_name() {
537 json_data.insert(
538 "build_name".to_string(),
539 serde_json::Value::String(name.to_string()),
540 );
541 }
542
543 let mut raw_config = serde_json::Map::new();
545 for key in config.config.keys() {
546 if let Some(value) = config.config.get_value(key) {
547 raw_config.insert(
548 key.to_string(),
549 serde_json::Value::String(value.to_string()),
550 );
551 }
552 }
553 json_data.insert(
554 "raw_config".to_string(),
555 serde_json::Value::Object(raw_config),
556 );
557
558 let mut hash_pairs = serde_json::Map::new();
560 for key in config.config.keys() {
561 if let Some(hash_pair) = config.config.get_hash_pair(key) {
562 hash_pairs.insert(
563 key.to_string(),
564 serde_json::json!({
565 "hash": hash_pair.hash,
566 "size": hash_pair.size
567 }),
568 );
569 }
570 }
571 if !hash_pairs.is_empty() {
572 json_data.insert(
573 "hash_pairs".to_string(),
574 serde_json::Value::Object(hash_pairs),
575 );
576 }
577
578 let output = match format {
579 OutputFormat::JsonPretty => serde_json::to_string_pretty(&json_data)?,
580 _ => serde_json::to_string(&json_data)?,
581 };
582
583 println!("{output}");
584 Ok(())
585}
586
587async fn inspect_cdn_config(
589 product: String,
590 region: String,
591 format: OutputFormat,
592) -> Result<(), Box<dyn std::error::Error>> {
593 let style = OutputStyle::new();
594 let region_enum = Region::from_str(®ion)?;
595
596 print_section_header(&format!("CDN Configuration Inspector - {product}"), &style);
597
598 let client = create_client(region_enum).await?;
600 let cdns_endpoint = Endpoint::ProductCdns(product.clone());
601 let cdns: ProductCdnsResponse = client.request_typed(&cdns_endpoint).await?;
602
603 let cdn_entry = cdns.entries.iter().find(|e| e.name == region);
605
606 let cdn_entry = match cdn_entry {
607 Some(entry) => entry,
608 None => {
609 eprintln!(
610 "{}",
611 format_warning(
612 &format!("No CDN configuration found for region {region}"),
613 &style
614 )
615 );
616 return Ok(());
617 }
618 };
619
620 match format {
621 OutputFormat::Json | OutputFormat::JsonPretty => {
622 let json_data = serde_json::json!({
623 "product": product,
624 "region": region,
625 "cdn_name": cdn_entry.name,
626 "path": cdn_entry.path,
627 "hosts": cdn_entry.hosts,
628 "config_path": &cdn_entry.config_path,
629 "total_hosts": cdn_entry.hosts.len(),
630 });
631
632 let output = match format {
633 OutputFormat::JsonPretty => serde_json::to_string_pretty(&json_data)?,
634 _ => serde_json::to_string(&json_data)?,
635 };
636 println!("{output}");
637 }
638 OutputFormat::Text => {
639 println!("{}", format_key_value("Product", &product, &style));
641 println!("{}", format_key_value("Region", ®ion, &style));
642 println!("{}", format_key_value("CDN Name", &cdn_entry.name, &style));
643 println!("{}", format_key_value("CDN Path", &cdn_entry.path, &style));
644 if !cdn_entry.config_path.is_empty() {
645 println!(
646 "{}",
647 format_key_value("Config Path", &cdn_entry.config_path, &style)
648 );
649 }
650
651 print_subsection_header(
653 &format!(
654 "Available CDN Hosts {}",
655 format_count_badge(cdn_entry.hosts.len(), "host", &style)
656 ),
657 &style,
658 );
659
660 let mut hosts_table = create_table(&style);
661 hosts_table.set_header(vec![
662 header_cell("Index", &style),
663 header_cell("CDN Host", &style),
664 header_cell("Status", &style),
665 ]);
666
667 for (i, host) in cdn_entry.hosts.iter().enumerate() {
668 let status = if host.contains("blzddist") || host.contains("battle.net") {
670 format_success("Active", &style)
671 } else {
672 "Unknown".to_string()
673 };
674
675 hosts_table.add_row(vec![
676 numeric_cell(&(i + 1).to_string()),
677 regular_cell(host),
678 regular_cell(&status),
679 ]);
680 }
681
682 println!("{hosts_table}");
683
684 print_subsection_header("Example CDN URLs", &style);
686
687 let primary_host = &cdn_entry.hosts[0];
688 let cdn_path = &cdn_entry.path;
689
690 println!("🌐 Base CDN URL:");
691 println!(" {primary_host}/{cdn_path}");
692 println!();
693 println!("📁 Common endpoints:");
694 println!(" Config: {primary_host}/{cdn_path}/config/[hash]");
695 println!(" Data: {primary_host}/{cdn_path}/data/[hash]");
696 println!(" Patch: {primary_host}/{cdn_path}/patch/[hash]");
697 }
698 OutputFormat::Bpsv => {
699 println!("## CDN Configuration");
701 println!("product:{product}");
702 println!("region:{region}");
703 println!("cdn_name:{}", cdn_entry.name);
704 println!("path:{}", cdn_entry.path);
705 if !cdn_entry.config_path.is_empty() {
706 println!("config_path:{}", cdn_entry.config_path);
707 }
708 for (i, host) in cdn_entry.hosts.iter().enumerate() {
709 println!("host_{i}:{host}");
710 }
711 }
712 }
713
714 Ok(())
715}
716
717async fn download_and_decompress_manifest(
719 product: &str,
720 region: &str,
721 manifest_type: &str,
722) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
723 let region = Region::from_str(region)?;
725 let client = create_client(region).await?;
726
727 let versions_endpoint = Endpoint::ProductVersions(product.to_string());
728 let versions: ProductVersionsResponse = client
729 .request_typed(&versions_endpoint)
730 .await
731 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
732
733 let entry = versions
734 .entries
735 .iter()
736 .find(|e| e.region == region.to_string())
737 .ok_or("Region not found")?;
738
739 let cdns_endpoint = Endpoint::ProductCdns(product.to_string());
741 let cdns: ProductCdnsResponse = client
742 .request_typed(&cdns_endpoint)
743 .await
744 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
745
746 let cdn_entry = cdns
747 .entries
748 .iter()
749 .find(|e| e.name == region.to_string())
750 .ok_or("CDN not found")?;
751
752 let cdn_client = CachedCdnClient::new().await?;
754 cdn_client.add_primary_hosts(cdn_entry.hosts.iter().cloned());
756 cdn_client.add_fallback_host("cdn.arctium.tools");
757 cdn_client.add_fallback_host("tact.mirror.reliquaryhq.com");
758 let cdn_host = &cdn_entry.hosts[0];
759 let cdn_path = &cdn_entry.path;
760
761 let config_response = cdn_client
762 .download_build_config(cdn_host, cdn_path, &entry.build_config)
763 .await?;
764 let config_text = config_response.text().await?;
765 let build_config = BuildConfig::parse(&config_text)?;
766
767 let manifest_hash = match manifest_type {
771 "encoding" => {
772 build_config
774 .config
775 .get_value("encoding")
776 .and_then(|v| v.split_whitespace().nth(1))
777 .ok_or("No encoding hash")?
778 }
779 "install" => build_config
780 .config
781 .get_value("install")
782 .and_then(|v| v.split_whitespace().nth(1))
783 .or_else(|| build_config.install_hash())
784 .ok_or("No install hash")?,
785 "download" => build_config
786 .config
787 .get_value("download")
788 .and_then(|v| v.split_whitespace().nth(1))
789 .or_else(|| build_config.download_hash())
790 .ok_or("No download hash")?,
791 "size" => build_config
792 .config
793 .get_value("size")
794 .and_then(|v| v.split_whitespace().nth(1))
795 .or_else(|| build_config.size_hash())
796 .ok_or("No size hash")?,
797 _ => return Err("Invalid manifest type".into()),
798 };
799
800 let response = cdn_client
803 .download_data(cdn_host, cdn_path, manifest_hash)
804 .await?;
805
806 let manifest_data = response.bytes().await?.to_vec();
807
808 if manifest_data.len() >= 4 && &manifest_data[0..4] == b"BLTE" {
810 let key_service = KeyService::new();
811 let decompressed = decompress_blte(manifest_data, Some(&key_service))?;
812 Ok(decompressed)
813 } else {
814 Ok(manifest_data)
815 }
816}
817
818async fn inspect_encoding(
820 product: String,
821 region: String,
822 stats: bool,
823 search: Option<String>,
824 _limit: usize,
825 format: OutputFormat,
826) -> Result<(), Box<dyn std::error::Error>> {
827 let style = OutputStyle::new();
828 print_section_header(&format!("Encoding File Inspector - {product}"), &style);
829
830 let encoding_data = download_and_decompress_manifest(&product, ®ion, "encoding").await?;
832
833 let encoding_file = EncodingFile::parse(&encoding_data)?;
835
836 match format {
837 OutputFormat::Json | OutputFormat::JsonPretty => {
838 let json_data = serde_json::json!({
839 "version": encoding_file.header.version,
840 "ckey_count": encoding_file.ckey_count(),
841 "ekey_count": encoding_file.ekey_count(),
842 "stats": if stats {
843 Some(serde_json::json!({
844 "total_ckeys": encoding_file.ckey_count(),
845 "total_ekeys": encoding_file.ekey_count(),
846 }))
847 } else {
848 None
849 },
850 });
851
852 let output = match format {
853 OutputFormat::JsonPretty => serde_json::to_string_pretty(&json_data)?,
854 _ => serde_json::to_string(&json_data)?,
855 };
856 println!("{output}");
857 }
858 OutputFormat::Text => {
859 print_subsection_header("Encoding File Summary", &style);
860 println!("Version: {}", encoding_file.header.version);
861 println!(
862 "CKey entries: {}",
863 format_count_badge(encoding_file.ckey_count(), "entry", &style)
864 );
865 println!(
866 "EKey mappings: {}",
867 format_count_badge(encoding_file.ekey_count(), "mapping", &style)
868 );
869
870 if let Some(search_key) = search {
871 print_subsection_header("Search Results", &style);
872 let search_bytes = hex::decode(&search_key)?;
873
874 if let Some(entry) = encoding_file.lookup_by_ckey(&search_bytes) {
875 println!("Found CKey: {search_key}");
876 println!(" File size: {} bytes", entry.size);
877 if !entry.encoding_keys.is_empty() {
878 println!(" EKeys:");
879 for ekey in &entry.encoding_keys {
880 println!(" - {}", hex::encode(ekey));
881 }
882 }
883 } else if let Some(ckey) = encoding_file.lookup_by_ekey(&search_bytes) {
884 println!("Found EKey: {search_key}");
885 println!(" Maps to CKey: {}", hex::encode(ckey));
886 } else {
887 println!("Key not found: {search_key}");
888 }
889 }
890
891 if stats {
892 print_subsection_header("Statistics", &style);
893 println!("Total unique content keys: {}", encoding_file.ckey_count());
894 println!(
895 "Total encoding key mappings: {}",
896 encoding_file.ekey_count()
897 );
898 }
899 }
900 _ => {
901 println!("Format not supported for encoding inspection");
902 }
903 }
904
905 Ok(())
906}
907
908async fn inspect_install(
910 product: String,
911 region: String,
912 tags: Option<String>,
913 all: bool,
914 format: OutputFormat,
915) -> Result<(), Box<dyn std::error::Error>> {
916 let style = OutputStyle::new();
917 print_section_header(&format!("Install Manifest Inspector - {product}"), &style);
918
919 let install_data = download_and_decompress_manifest(&product, ®ion, "install").await?;
921
922 let install_manifest = InstallManifest::parse(&install_data)?;
924
925 match format {
926 OutputFormat::Json | OutputFormat::JsonPretty => {
927 let json_data = serde_json::json!({
928 "version": install_manifest.header.version,
929 "entry_count": install_manifest.entries.len(),
930 "tag_count": install_manifest.tags.len(),
931 "tags": install_manifest.tags.iter().map(|t| &t.name).collect::<Vec<_>>(),
932 });
933
934 let output = match format {
935 OutputFormat::JsonPretty => serde_json::to_string_pretty(&json_data)?,
936 _ => serde_json::to_string(&json_data)?,
937 };
938 println!("{output}");
939 }
940 OutputFormat::Text => {
941 print_subsection_header("Install Manifest Summary", &style);
942 println!("Version: {}", install_manifest.header.version);
943 println!(
944 "Total files: {}",
945 format_count_badge(install_manifest.entries.len(), "file", &style)
946 );
947 println!(
948 "Total tags: {}",
949 format_count_badge(install_manifest.tags.len(), "tag", &style)
950 );
951
952 if !install_manifest.tags.is_empty() {
953 print_subsection_header("Available Tags", &style);
954 for tag in &install_manifest.tags {
955 println!(" - {} (type: {})", tag.name, tag.tag_type);
956 }
957 }
958
959 if let Some(tag_filter) = tags {
960 let filter_tags: Vec<&str> = tag_filter.split(',').collect();
961 let filtered_files = install_manifest.get_files_for_tags(&filter_tags);
962
963 print_subsection_header(&format!("Files for tags: {tag_filter}"), &style);
964 println!(
965 "Found {} files",
966 format_count_badge(filtered_files.len(), "file", &style)
967 );
968
969 if all || filtered_files.len() <= 20 {
970 for (i, file) in filtered_files.iter().enumerate() {
971 if i >= 20 && !all {
972 println!("... and {} more", filtered_files.len() - i);
973 break;
974 }
975 println!(" {}", file.path);
976 }
977 }
978 }
979 }
980 _ => {
981 println!("Format not supported for install manifest inspection");
982 }
983 }
984
985 Ok(())
986}
987
988async fn inspect_download_manifest(
990 product: String,
991 region: String,
992 priority_limit: usize,
993 tags: Option<String>,
994 format: OutputFormat,
995) -> Result<(), Box<dyn std::error::Error>> {
996 let style = OutputStyle::new();
997 print_section_header(&format!("Download Manifest Inspector - {product}"), &style);
998
999 let download_data = download_and_decompress_manifest(&product, ®ion, "download").await?;
1001
1002 let download_manifest = DownloadManifest::parse(&download_data)?;
1004
1005 match format {
1006 OutputFormat::Json | OutputFormat::JsonPretty => {
1007 let priority_files =
1008 download_manifest.get_priority_files(priority_limit.min(127) as i8);
1009 let json_data = serde_json::json!({
1010 "version": download_manifest.header.version,
1011 "entry_count": download_manifest.entries.len(),
1012 "tag_count": download_manifest.tags.len(),
1013 "priority_files": priority_files.iter().map(|entry| {
1014 serde_json::json!({
1015 "ekey": hex::encode(&entry.ekey),
1016 "priority": entry.priority,
1017 })
1018 }).collect::<Vec<_>>(),
1019 });
1020
1021 let output = match format {
1022 OutputFormat::JsonPretty => serde_json::to_string_pretty(&json_data)?,
1023 _ => serde_json::to_string(&json_data)?,
1024 };
1025 println!("{output}");
1026 }
1027 OutputFormat::Text => {
1028 print_subsection_header("Download Manifest Summary", &style);
1029 println!("Version: {}", download_manifest.header.version);
1030 println!(
1031 "Total entries: {}",
1032 format_count_badge(download_manifest.entries.len(), "entry", &style)
1033 );
1034 println!(
1035 "Total tags: {}",
1036 format_count_badge(download_manifest.tags.len(), "tag", &style)
1037 );
1038
1039 print_subsection_header(&format!("Top {priority_limit} Priority Files"), &style);
1040 let priority_files =
1041 download_manifest.get_priority_files(priority_limit.min(127) as i8);
1042 for (i, entry) in priority_files.iter().enumerate() {
1043 println!(
1044 " {}. Priority {}: {}",
1045 i + 1,
1046 entry.priority,
1047 hex::encode(&entry.ekey)
1048 );
1049 }
1050
1051 if let Some(tag_filter) = tags {
1052 let filter_tags: Vec<&str> = tag_filter.split(',').collect();
1053 let filtered_files = download_manifest.get_files_for_tags(&filter_tags);
1054
1055 print_subsection_header(&format!("Files for tags: {tag_filter}"), &style);
1056 println!(
1057 "Found {} files",
1058 format_count_badge(filtered_files.len(), "file", &style)
1059 );
1060 }
1061 }
1062 _ => {
1063 println!("Format not supported for download manifest inspection");
1064 }
1065 }
1066
1067 Ok(())
1068}
1069
1070async fn inspect_size(
1072 product: String,
1073 region: String,
1074 largest: usize,
1075 tags: Option<String>,
1076 format: OutputFormat,
1077) -> Result<(), Box<dyn std::error::Error>> {
1078 let style = OutputStyle::new();
1079 print_section_header(&format!("Size File Inspector - {product}"), &style);
1080
1081 let size_data = download_and_decompress_manifest(&product, ®ion, "size").await?;
1083
1084 let size_file = SizeFile::parse(&size_data)?;
1086
1087 match format {
1088 OutputFormat::Json | OutputFormat::JsonPretty => {
1089 let largest_files = size_file.get_largest_files(largest);
1090 let stats = size_file.get_statistics();
1091 let json_data = serde_json::json!({
1092 "version": size_file.header.version,
1093 "entry_count": size_file.entries.len(),
1094 "tag_count": size_file.tags.len(),
1095 "total_size": size_file.get_total_size(),
1096 "statistics": {
1097 "average_size": stats.average_size,
1098 "min_size": stats.min_size,
1099 "max_size": stats.max_size,
1100 },
1101 "largest_files": largest_files.iter().map(|(ekey, size)| {
1102 serde_json::json!({
1103 "ekey": hex::encode(ekey),
1104 "size": size,
1105 })
1106 }).collect::<Vec<_>>(),
1107 });
1108
1109 let output = match format {
1110 OutputFormat::JsonPretty => serde_json::to_string_pretty(&json_data)?,
1111 _ => serde_json::to_string(&json_data)?,
1112 };
1113 println!("{output}");
1114 }
1115 OutputFormat::Text => {
1116 print_subsection_header("Size File Summary", &style);
1117 println!("Version: {}", size_file.header.version);
1118 println!(
1119 "Total entries: {}",
1120 format_count_badge(size_file.entries.len(), "entry", &style)
1121 );
1122 println!(
1123 "Total tags: {}",
1124 format_count_badge(size_file.tags.len(), "tag", &style)
1125 );
1126
1127 let total_size = size_file.get_total_size();
1128 println!(
1129 "Total installation size: {} GB",
1130 total_size / (1024 * 1024 * 1024)
1131 );
1132
1133 let stats = size_file.get_statistics();
1134 print_subsection_header("File Size Statistics", &style);
1135 println!(
1136 "Average file size: {} MB",
1137 stats.average_size / (1024 * 1024)
1138 );
1139 println!("Minimum file size: {} bytes", stats.min_size);
1140 println!("Maximum file size: {} MB", stats.max_size / (1024 * 1024));
1141
1142 print_subsection_header(&format!("Top {largest} Largest Files"), &style);
1143 let largest_files = size_file.get_largest_files(largest);
1144 for (i, (ekey, size)) in largest_files.iter().enumerate() {
1145 let size_mb = size / (1024 * 1024);
1146 println!(" {}. {} MB - {}", i + 1, size_mb, hex::encode(&ekey[0..8]));
1147 }
1148
1149 if let Some(tag_filter) = tags {
1150 let filter_tags: Vec<&str> = tag_filter.split(',').collect();
1151 let tag_size = size_file.get_size_for_tags(&filter_tags);
1152
1153 print_subsection_header(&format!("Size for tags: {tag_filter}"), &style);
1154 println!("Total size: {} GB", tag_size / (1024 * 1024 * 1024));
1155 }
1156 }
1157 _ => {
1158 println!("Format not supported for size file inspection");
1159 }
1160 }
1161
1162 Ok(())
1163}