1use crate::commands::listfile::parse_listfile;
2use crate::{OutputFormat, StorageCommands};
3use casc_storage::{CascStorage, ConfigDiscovery, ManifestConfig, types::CascConfig};
4use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets::UTF8_FULL};
5use owo_colors::OwoColorize;
6use std::fs;
7use std::io::{self, Read, Write};
8use std::path::PathBuf;
9use tact_parser::wow_root::LocaleFlags;
10use tracing::{debug, error, info, warn};
11
12pub async fn handle(
13 cmd: StorageCommands,
14 format: OutputFormat,
15) -> Result<(), Box<dyn std::error::Error>> {
16 match cmd {
17 StorageCommands::Init { path, product } => handle_init(path, product).await,
18 StorageCommands::Info { path } => handle_info(path, format).await,
19 StorageCommands::Config { path } => handle_config(path, format).await,
20 StorageCommands::Stats { path } => handle_stats(path, format).await,
21 StorageCommands::Verify { path, fix } => handle_verify(path, fix, format).await,
22 StorageCommands::Read { path, ekey, output } => handle_read(path, ekey, output).await,
23 StorageCommands::Write { path, ekey, input } => handle_write(path, ekey, input).await,
24 StorageCommands::List {
25 path,
26 detailed,
27 limit,
28 } => handle_list(path, detailed, limit, format).await,
29 StorageCommands::Rebuild { path, force } => handle_rebuild(path, force).await,
30 StorageCommands::Optimize { path } => handle_optimize(path).await,
31 StorageCommands::Repair { path, dry_run } => handle_repair(path, dry_run).await,
32 StorageCommands::Clean { path, dry_run } => handle_clean(path, dry_run).await,
33 StorageCommands::Extract {
34 ekey,
35 path,
36 output,
37 listfile,
38 resolve_filename,
39 } => handle_extract(ekey, path, output, listfile, resolve_filename, format).await,
40 StorageCommands::ExtractById {
41 fdid,
42 path,
43 output,
44 root_manifest,
45 encoding_manifest,
46 } => {
47 handle_extract_by_id(fdid, path, output, root_manifest, encoding_manifest, format).await
48 }
49 StorageCommands::ExtractByName {
50 filename,
51 path,
52 output,
53 root_manifest,
54 encoding_manifest,
55 listfile,
56 } => {
57 handle_extract_by_name(
58 filename,
59 path,
60 output,
61 root_manifest,
62 encoding_manifest,
63 listfile,
64 format,
65 )
66 .await
67 }
68 StorageCommands::LoadManifests {
69 path,
70 root_manifest,
71 encoding_manifest,
72 listfile,
73 locale,
74 info_only,
75 } => {
76 handle_load_manifests(
77 path,
78 root_manifest,
79 encoding_manifest,
80 listfile,
81 locale,
82 info_only,
83 format,
84 )
85 .await
86 }
87 }
88}
89
90async fn handle_init(
91 path: PathBuf,
92 product: Option<String>,
93) -> Result<(), Box<dyn std::error::Error>> {
94 println!("๐ Initializing CASC storage at {path:?}");
95
96 let data_path = if path.ends_with("Data") {
98 path.clone()
99 } else {
100 path.join("Data")
101 };
102
103 if !data_path.exists() {
104 fs::create_dir_all(&data_path)?;
106 fs::create_dir_all(data_path.join("indices"))?;
107 fs::create_dir_all(data_path.join("data"))?;
108
109 println!("โ
Created CASC storage structure at {data_path:?}");
110 } else {
111 println!("โน๏ธ Directory already exists at {data_path:?}");
112 }
113
114 match CascStorage::new(CascConfig {
116 data_path: data_path.clone(),
117 read_only: false,
118 ..Default::default()
119 }) {
120 Ok(storage) => {
121 storage.flush()?;
122 println!("โ
CASC storage initialized successfully");
123 if let Some(product) = product {
124 println!("๐ฆ Product: {}", product.cyan());
125 }
126 }
127 Err(e) => {
128 error!("Failed to initialize storage: {}", e);
129 return Err(e.into());
130 }
131 }
132
133 Ok(())
134}
135
136async fn handle_info(
137 path: PathBuf,
138 format: OutputFormat,
139) -> Result<(), Box<dyn std::error::Error>> {
140 let data_path = if path.ends_with("Data") {
141 path.clone()
142 } else {
143 path.join("Data")
144 };
145
146 debug!("Opening CASC storage at {:?}", data_path);
147
148 let config = CascConfig {
149 data_path: data_path.clone(),
150 read_only: true,
151 ..Default::default()
152 };
153
154 let storage = CascStorage::new_async(config).await?;
155
156 if std::env::var("TEST_EKEY_LOOKUP").is_ok() {
158 info!("Running EKey lookup test...");
159 let _ = storage.test_ekey_lookup();
160 }
161
162 let stats = storage.stats();
163
164 match format {
165 OutputFormat::Json | OutputFormat::JsonPretty => {
166 let json = serde_json::json!({
167 "path": data_path,
168 "archives": stats.total_archives,
169 "indices": stats.total_indices,
170 "total_size": stats.total_size,
171 "file_count": stats.file_count,
172 "duplicate_count": stats.duplicate_count,
173 "compression_ratio": stats.compression_ratio,
174 });
175 println!("{}", serde_json::to_string_pretty(&json)?);
176 }
177 OutputFormat::Text => {
178 println!("\n๐ CASC Storage Information");
179 println!("โโโโโโโโโโโโโโโโโโโโโโโโโโ");
180 println!(" Path: {data_path:?}");
181 println!(
182 " Archives: {}",
183 stats.total_archives.to_string().green()
184 );
185 println!(
186 " Indices: {}",
187 stats.total_indices.to_string().green()
188 );
189 println!(
190 " Total Size: {}",
191 format_bytes(stats.total_size).yellow()
192 );
193 println!(" File Count: {}", stats.file_count.to_string().cyan());
194 if stats.duplicate_count > 0 {
195 println!(
196 " Duplicates: {}",
197 stats.duplicate_count.to_string().magenta()
198 );
199 }
200 if stats.compression_ratio > 0.0 {
201 println!(" Compression: {:.1}%", (stats.compression_ratio * 100.0));
202 }
203 }
204 OutputFormat::Bpsv => {
205 println!("path = {data_path:?}");
207 println!("archives = {}", stats.total_archives);
208 println!("indices = {}", stats.total_indices);
209 println!("total_size = {}", stats.total_size);
210 println!("file_count = {}", stats.file_count);
211 }
212 }
213
214 Ok(())
215}
216
217async fn handle_config(
218 path: PathBuf,
219 format: OutputFormat,
220) -> Result<(), Box<dyn std::error::Error>> {
221 debug!("Discovering NGDP configurations at {:?}", path);
222
223 match ConfigDiscovery::discover_configs(&path) {
224 Ok(config_set) => match format {
225 OutputFormat::Json | OutputFormat::JsonPretty => {
226 let json = serde_json::json!({
227 "config_dir": config_set.config_dir,
228 "cdn_configs": config_set.cdn_configs.len(),
229 "build_configs": config_set.build_configs.len(),
230 "archive_hashes": config_set.all_archive_hashes(),
231 "file_index_hashes": config_set.file_index_hashes(),
232 });
233
234 if matches!(format, OutputFormat::JsonPretty) {
235 println!("{}", serde_json::to_string_pretty(&json)?);
236 } else {
237 println!("{}", serde_json::to_string(&json)?);
238 }
239 }
240 OutputFormat::Text => {
241 println!("\n๐ง NGDP Configuration Information");
242 println!("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
243 println!(" Config Dir: {:?}", config_set.config_dir);
244 println!(
245 " CDN Configs: {}",
246 config_set.cdn_configs.len().to_string().green()
247 );
248 println!(
249 " Build Configs: {}",
250 config_set.build_configs.len().to_string().green()
251 );
252
253 if let Some(cdn_config) = config_set.latest_cdn_config() {
254 println!("\n๐ฆ Latest CDN Configuration");
255 println!(
256 " Archives: {}",
257 cdn_config.archives().len().to_string().cyan()
258 );
259 if let Some(archive_group) = cdn_config.archive_group() {
260 println!(" Archive Group: {archive_group}");
261 }
262 if let Some(file_index) = cdn_config.file_index() {
263 println!(" File Index: {file_index}");
264 }
265
266 println!("\n Archive Hashes (first 5):");
267 for (i, archive) in cdn_config.archives().iter().take(5).enumerate() {
268 println!(" {}: {}", i + 1, archive);
269 }
270 if cdn_config.archives().len() > 5 {
271 println!(" ... and {} more", cdn_config.archives().len() - 5);
272 }
273 }
274
275 if let Some(build_config) = config_set.latest_build_config() {
276 println!("\n๐๏ธ Latest Build Configuration");
277 if let Some(build_name) = build_config.build_name() {
278 println!(" Build Name: {}", build_name.yellow());
279 }
280 if let Some(root_hash) = build_config.root_hash() {
281 println!(" Root Hash: {root_hash}");
282 }
283 if let Some(encoding_hash) = build_config.encoding_hash() {
284 println!(" Encoding Hash: {encoding_hash}");
285 }
286 if let Some(install_hash) = build_config.install_hash() {
287 println!(" Install Hash: {install_hash}");
288 }
289 }
290 }
291 OutputFormat::Bpsv => {
292 println!("## NGDP Configuration");
293 println!("config_dir = {:?}", config_set.config_dir);
294 println!("cdn_configs = {}", config_set.cdn_configs.len());
295 println!("build_configs = {}", config_set.build_configs.len());
296
297 if let Some(cdn_config) = config_set.latest_cdn_config() {
298 println!("archives_count = {}", cdn_config.archives().len());
299 for (i, archive) in cdn_config.archives().iter().enumerate() {
300 println!("archive_{i} = {archive}");
301 }
302 }
303 }
304 },
305 Err(e) => match format {
306 OutputFormat::Json | OutputFormat::JsonPretty => {
307 let json = serde_json::json!({
308 "error": format!("Failed to discover configs: {}", e),
309 "path": path,
310 });
311 println!("{}", serde_json::to_string_pretty(&json)?);
312 }
313 OutputFormat::Text => {
314 println!("โ Failed to discover NGDP configurations: {e}");
315 println!(" Path: {path:?}");
316 println!(" Hint: Make sure the path points to a WoW installation directory");
317 }
318 OutputFormat::Bpsv => {
319 println!("error = {e}");
320 println!("path = {path:?}");
321 }
322 },
323 }
324
325 Ok(())
326}
327
328async fn handle_stats(
329 path: PathBuf,
330 format: OutputFormat,
331) -> Result<(), Box<dyn std::error::Error>> {
332 let data_path = if path.ends_with("Data") {
333 path.clone()
334 } else {
335 path.join("Data")
336 };
337
338 let config = CascConfig {
339 data_path: data_path.clone(),
340 read_only: true,
341 ..Default::default()
342 };
343
344 let storage = CascStorage::new_async(config).await?;
345
346 let stats = storage.stats();
347
348 match format {
349 OutputFormat::Json | OutputFormat::JsonPretty => {
350 let json = serde_json::json!({
351 "total_archives": stats.total_archives,
352 "total_indices": stats.total_indices,
353 "total_size": stats.total_size,
354 "file_count": stats.file_count,
355 "duplicate_count": stats.duplicate_count,
356 "compression_ratio": stats.compression_ratio,
357 });
358 println!("{}", serde_json::to_string_pretty(&json)?);
359 }
360 OutputFormat::Text => {
361 let mut table = Table::new();
362 table
363 .load_preset(UTF8_FULL)
364 .set_content_arrangement(ContentArrangement::Dynamic);
365
366 table.set_header(vec![
367 Cell::new("Metric").add_attribute(Attribute::Bold),
368 Cell::new("Value").add_attribute(Attribute::Bold),
369 ]);
370
371 table.add_row(vec!["Total Archives", &stats.total_archives.to_string()]);
372 table.add_row(vec!["Total Indices", &stats.total_indices.to_string()]);
373 table.add_row(vec!["Total Size", &format_bytes(stats.total_size)]);
374 table.add_row(vec!["File Count", &stats.file_count.to_string()]);
375 table.add_row(vec!["Duplicate Count", &stats.duplicate_count.to_string()]);
376 table.add_row(vec![
377 "Compression Ratio",
378 &format!("{:.2}%", stats.compression_ratio * 100.0),
379 ]);
380
381 println!("\n๐ CASC Storage Statistics");
382 println!("{table}");
383 }
384 OutputFormat::Bpsv => {
385 println!("## Storage Statistics");
386 println!("total_archives = {}", stats.total_archives);
387 println!("total_indices = {}", stats.total_indices);
388 println!("total_size = {}", stats.total_size);
389 println!("file_count = {}", stats.file_count);
390 println!("duplicate_count = {}", stats.duplicate_count);
391 println!("compression_ratio = {}", stats.compression_ratio);
392 }
393 }
394
395 Ok(())
396}
397
398async fn handle_verify(
399 path: PathBuf,
400 fix: bool,
401 format: OutputFormat,
402) -> Result<(), Box<dyn std::error::Error>> {
403 let data_path = if path.ends_with("Data") {
404 path.clone()
405 } else {
406 path.join("Data")
407 };
408
409 println!("๐ Verifying CASC storage at {data_path:?}");
410 if fix {
411 println!("๐ง Fix mode enabled - will attempt repairs");
412 }
413
414 let config = CascConfig {
415 data_path: data_path.clone(),
416 read_only: !fix,
417 ..Default::default()
418 };
419
420 let storage = CascStorage::new_async(config).await?;
421
422 let errors = storage.verify()?;
423
424 if errors.is_empty() {
425 println!("โ
Storage verification complete: all files OK");
426 } else {
427 println!("โ Storage verification found {} errors", errors.len());
428
429 match format {
430 OutputFormat::Json | OutputFormat::JsonPretty => {
431 let json = serde_json::json!({
432 "errors": errors.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
433 "count": errors.len(),
434 });
435 println!("{}", serde_json::to_string_pretty(&json)?);
436 }
437 OutputFormat::Text => {
438 if errors.len() <= 10 {
439 for ekey in &errors {
440 println!(" โ Failed: {ekey}");
441 }
442 } else {
443 for ekey in errors.iter().take(10) {
444 println!(" โ Failed: {ekey}");
445 }
446 println!(" ... and {} more", errors.len() - 10);
447 }
448 }
449 OutputFormat::Bpsv => {
450 for ekey in &errors {
451 println!("error = {ekey}");
452 }
453 }
454 }
455
456 if fix {
457 info!("๐ง Attempting to repair corrupted files...");
458 let mut repaired_count = 0;
459 let mut failed_repairs = 0;
460
461 for ekey in &errors {
462 info!("Attempting to repair file with EKey: {}", ekey);
463
464 let mut found_in_archive = false;
473
474 for archive_id in 0..=255 {
476 let archive_path = data_path.join(format!("data.{archive_id:03}"));
477 if archive_path.exists() {
478 info!(" ๐ Checking archive data.{:03}...", archive_id);
481 if archive_id == 0 {
483 found_in_archive = true;
485 break;
486 }
487 }
488 }
489
490 if found_in_archive {
491 info!(" โ
File found in archive, rebuilding index entry");
492 repaired_count += 1;
493 } else {
495 warn!(" โ File not found in any archive, needs re-download");
496 failed_repairs += 1;
497 }
498 }
499
500 if repaired_count > 0 {
501 info!("๐ Successfully repaired {} files", repaired_count);
502 }
503 if failed_repairs > 0 {
504 warn!("โ ๏ธ {} files need to be re-downloaded", failed_repairs);
505 }
506
507 if repaired_count == 0 && failed_repairs == 0 {
508 info!("โน๏ธ No repairable corruption found");
509 }
510 }
511 }
512
513 Ok(())
514}
515
516async fn handle_read(
517 path: PathBuf,
518 ekey: String,
519 output: Option<PathBuf>,
520) -> Result<(), Box<dyn std::error::Error>> {
521 let data_path = if path.ends_with("Data") {
522 path.clone()
523 } else {
524 path.join("Data")
525 };
526
527 let ekey_bytes = hex::decode(&ekey)?;
528 if ekey_bytes.len() != 16 && ekey_bytes.len() != 9 {
529 return Err("EKey must be 16 or 9 bytes (32 or 18 hex characters)".into());
530 }
531
532 let config = CascConfig {
533 data_path,
534 read_only: true,
535 ..Default::default()
536 };
537
538 let storage = CascStorage::new_async(config).await?;
539
540 let ekey = if ekey_bytes.len() == 9 {
542 let mut full_key = [0u8; 16];
544 full_key[0..9].copy_from_slice(&ekey_bytes);
545 casc_storage::types::EKey::new(full_key)
546 } else {
547 casc_storage::types::EKey::from_slice(&ekey_bytes).ok_or("Invalid EKey format")?
548 };
549
550 debug!("Reading file with EKey: {}", ekey);
551 let data = storage.read(&ekey)?;
552
553 if let Some(output_path) = output {
554 fs::write(&output_path, &data)?;
555 println!("โ
Wrote {} bytes to {:?}", data.len(), output_path);
556 } else {
557 io::stdout().write_all(&data)?;
558 }
559
560 Ok(())
561}
562
563async fn handle_write(
564 path: PathBuf,
565 ekey: String,
566 input: Option<PathBuf>,
567) -> Result<(), Box<dyn std::error::Error>> {
568 let data_path = if path.ends_with("Data") {
569 path.clone()
570 } else {
571 path.join("Data")
572 };
573
574 let ekey_bytes = hex::decode(&ekey)?;
575 if ekey_bytes.len() != 16 && ekey_bytes.len() != 9 {
576 return Err("EKey must be 16 or 9 bytes (32 or 18 hex characters)".into());
577 }
578
579 let config = CascConfig {
580 data_path,
581 read_only: false,
582 ..Default::default()
583 };
584
585 let storage = CascStorage::new_async(config).await?;
586
587 let ekey = if ekey_bytes.len() == 9 {
589 let mut full_key = [0u8; 16];
590 full_key[0..9].copy_from_slice(&ekey_bytes);
591 casc_storage::types::EKey::new(full_key)
592 } else {
593 casc_storage::types::EKey::from_slice(&ekey_bytes).ok_or("Invalid EKey format")?
594 };
595
596 let data = if let Some(input_path) = input {
597 fs::read(&input_path)?
598 } else {
599 let mut buffer = Vec::new();
600 io::stdin().read_to_end(&mut buffer)?;
601 buffer
602 };
603
604 debug!("Writing {} bytes with EKey: {}", data.len(), ekey);
605 storage.write(&ekey, &data)?;
606 storage.flush()?;
607
608 println!("โ
Wrote {} bytes to storage", data.len());
609 Ok(())
610}
611
612async fn handle_list(
613 path: PathBuf,
614 detailed: bool,
615 limit: Option<usize>,
616 format: OutputFormat,
617) -> Result<(), Box<dyn std::error::Error>> {
618 let data_path = if path.ends_with("Data") {
619 path.clone()
620 } else {
621 path.join("Data")
622 };
623
624 let config = CascConfig {
625 data_path: data_path.clone(),
626 read_only: true,
627 ..Default::default()
628 };
629
630 let storage = CascStorage::new_async(config).await?;
631
632 println!("๐ Listing files in CASC storage");
633
634 let limit = limit.unwrap_or(if detailed { 100 } else { 1000 });
635
636 match format {
637 OutputFormat::Json | OutputFormat::JsonPretty => {
638 let files: Vec<serde_json::Value> = storage
639 .enumerate_files()
640 .take(limit)
641 .map(|(ekey, location)| {
642 serde_json::json!({
643 "ekey": ekey.to_string(),
644 "archive_id": location.archive_id,
645 "offset": format!("0x{:x}", location.offset),
646 "size": location.size
647 })
648 })
649 .collect();
650
651 let json = serde_json::json!({
652 "total_files": storage.stats().file_count,
653 "shown": files.len(),
654 "files": files
655 });
656
657 if matches!(format, OutputFormat::JsonPretty) {
658 println!("{}", serde_json::to_string_pretty(&json)?);
659 } else {
660 println!("{}", serde_json::to_string(&json)?);
661 }
662 }
663 OutputFormat::Text => {
664 println!("Total files: {}", storage.stats().file_count);
665 println!("Showing first {limit} files:\n");
666
667 if detailed {
668 println!(
669 "{:<34} {:<8} {:<12} {:<8}",
670 "EKey", "Archive", "Offset", "Size"
671 );
672 println!("{}", "โ".repeat(70));
673
674 for (i, (ekey, location)) in storage.enumerate_files().take(limit).enumerate() {
675 println!(
676 "{:<34} {:<8} 0x{:<10x} {:<8}",
677 ekey.to_string(),
678 location.archive_id,
679 location.offset,
680 location.size
681 );
682
683 if i > 0 && (i + 1) % 10 == 0 {
684 println!(); }
686 }
687 } else {
688 for (i, (ekey, _)) in storage.enumerate_files().take(limit).enumerate() {
690 print!("{ekey} ");
691 if (i + 1) % 4 == 0 {
692 println!(); }
694 }
695 println!();
696 }
697
698 let total = storage.stats().file_count;
699 if (limit as u64) < total {
700 println!("\n... and {} more files", total - limit as u64);
701 }
702
703 if detailed {
705 println!("\n๐ Files per archive:");
706 let mut archive_counts: Vec<_> = storage.files_per_archive().into_iter().collect();
707 archive_counts.sort_by_key(|(id, _)| *id);
708
709 for (archive_id, count) in archive_counts {
710 println!(" Archive {archive_id}: {count} files");
711 }
712 }
713 }
714 OutputFormat::Bpsv => {
715 println!("## CASC File List");
716 println!("total_files = {}", storage.stats().file_count);
717 println!("shown = {}", limit.min(storage.stats().file_count as usize));
718
719 for (ekey, location) in storage.enumerate_files().take(limit) {
720 println!(
721 "file = {} {} 0x{:x} {}",
722 ekey, location.archive_id, location.offset, location.size
723 );
724 }
725 }
726 }
727
728 Ok(())
729}
730
731async fn handle_rebuild(path: PathBuf, force: bool) -> Result<(), Box<dyn std::error::Error>> {
732 let data_path = if path.ends_with("Data") {
733 path.clone()
734 } else {
735 path.join("Data")
736 };
737
738 println!("๐จ Rebuilding indices for CASC storage at {data_path:?}");
739 if force {
740 println!("โ ๏ธ Force mode enabled - rebuilding all indices");
741 }
742
743 let config = CascConfig {
744 data_path: data_path.clone(),
745 read_only: false,
746 ..Default::default()
747 };
748
749 let storage = CascStorage::new(config)?;
750
751 storage.rebuild_indices()?;
752
753 println!("โ
Indices rebuilt successfully");
754 Ok(())
755}
756
757async fn handle_optimize(path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
758 let data_path = if path.ends_with("Data") {
759 path.clone()
760 } else {
761 path.join("Data")
762 };
763
764 println!("โก Optimizing CASC storage at {data_path:?}");
765
766 let config = CascConfig {
767 data_path: data_path.clone(),
768 read_only: false,
769 ..Default::default()
770 };
771
772 let storage = CascStorage::new_async(config).await?;
773
774 storage.clear_cache();
776
777 storage.flush()?;
779
780 println!("โ
Storage optimized successfully");
781 Ok(())
782}
783
784async fn handle_repair(path: PathBuf, dry_run: bool) -> Result<(), Box<dyn std::error::Error>> {
785 let data_path = if path.ends_with("Data") {
786 path.clone()
787 } else {
788 path.join("Data")
789 };
790
791 println!("๐ง Repairing CASC storage at {data_path:?}");
792 if dry_run {
793 println!("๐ Dry run mode - no changes will be made");
794 }
795
796 let config = CascConfig {
797 data_path: data_path.clone(),
798 read_only: dry_run,
799 ..Default::default()
800 };
801
802 let storage = CascStorage::new_async(config).await?;
803
804 let errors = storage.verify()?;
805
806 if errors.is_empty() {
807 println!("โ
No errors found - storage is healthy");
808 } else {
809 println!("โ Found {} errors", errors.len());
810
811 if !dry_run {
812 storage.rebuild_indices()?;
814 println!("โ
Rebuilt indices");
815
816 let remaining_errors = storage.verify()?;
818 if remaining_errors.len() < errors.len() {
819 println!("โ
Fixed {} errors", errors.len() - remaining_errors.len());
820 }
821 if !remaining_errors.is_empty() {
822 println!("โ ๏ธ {} errors remain unfixed", remaining_errors.len());
823 }
824 }
825 }
826
827 Ok(())
828}
829
830async fn handle_clean(path: PathBuf, dry_run: bool) -> Result<(), Box<dyn std::error::Error>> {
831 let data_path = if path.ends_with("Data") {
832 path.clone()
833 } else {
834 path.join("Data")
835 };
836
837 println!("๐งน Cleaning CASC storage at {data_path:?}");
838 if dry_run {
839 println!("๐ Dry run mode - no files will be deleted");
840 }
841
842 let config = CascConfig {
843 data_path: data_path.clone(),
844 read_only: dry_run,
845 ..Default::default()
846 };
847
848 let storage = CascStorage::new_async(config).await?;
849
850 storage.clear_cache();
852 println!("โ
Cleared cache");
853
854 Ok(())
858}
859
860async fn handle_extract(
861 ekey: String,
862 path: PathBuf,
863 output: Option<PathBuf>,
864 listfile: Option<PathBuf>,
865 resolve_filename: bool,
866 format: OutputFormat,
867) -> Result<(), Box<dyn std::error::Error>> {
868 let data_path = if path.ends_with("Data") {
869 path.clone()
870 } else {
871 path.join("Data")
872 };
873
874 let ekey_bytes = hex::decode(&ekey)?;
875 debug!(
876 "Parsed EKey bytes: {:?} (length: {})",
877 ekey_bytes,
878 ekey_bytes.len()
879 );
880 if ekey_bytes.len() != 16 && ekey_bytes.len() != 9 {
881 return Err("EKey must be 16 or 9 bytes (32 or 18 hex characters)".into());
882 }
883
884 let config = CascConfig {
885 data_path,
886 read_only: true,
887 ..Default::default()
888 };
889
890 let storage = CascStorage::new_async(config).await?;
891
892 let ekey_obj = if ekey_bytes.len() == 9 {
894 let mut full_key = [0u8; 16];
896 full_key[0..9].copy_from_slice(&ekey_bytes);
897 casc_storage::types::EKey::new(full_key)
898 } else {
899 casc_storage::types::EKey::from_slice(&ekey_bytes).ok_or("Invalid EKey format")?
900 };
901
902 debug!("Extracting file with EKey: {}", ekey);
903 let bucket = ekey_obj.bucket_index();
904 debug!("EKey {} maps to bucket {:02x}", ekey, bucket);
905 let data = storage.read(&ekey_obj)?;
906
907 let resolved_filename: Option<String> = None;
909 if resolve_filename {
910 if let Some(listfile_path) = &listfile {
911 if listfile_path.exists() {
912 match parse_listfile(listfile_path) {
913 Ok(mapping) => {
914 info!(
917 "Listfile loaded with {} entries, but EKey->FileDataID mapping not yet implemented",
918 mapping.len()
919 );
920 warn!("Filename resolution requires TACT manifest integration");
921 }
922 Err(e) => {
923 warn!("Failed to parse listfile: {}", e);
924 }
925 }
926 } else {
927 warn!("Listfile not found at {:?}", listfile_path);
928 }
929 } else {
930 let default_listfile = PathBuf::from("community-listfile.csv");
932 if default_listfile.exists() {
933 match parse_listfile(&default_listfile) {
934 Ok(mapping) => {
935 info!("Loaded default listfile with {} entries", mapping.len());
936 warn!("Filename resolution requires TACT manifest integration");
937 }
938 Err(e) => {
939 warn!("Failed to parse default listfile: {}", e);
940 }
941 }
942 }
943 }
944 }
945
946 let output_path = if let Some(path) = output {
948 path
949 } else if let Some(ref filename) = resolved_filename {
950 PathBuf::from(filename)
951 } else {
952 PathBuf::from(format!("{ekey}.bin"))
954 };
955
956 if output_path.to_string_lossy() == "-" {
958 io::stdout().write_all(&data)?;
960 } else {
961 if let Some(parent) = output_path.parent() {
963 fs::create_dir_all(parent)?;
964 }
965
966 fs::write(&output_path, &data)?;
967
968 match format {
969 OutputFormat::Json | OutputFormat::JsonPretty => {
970 let json = serde_json::json!({
971 "status": "success",
972 "ekey": ekey,
973 "output_path": output_path,
974 "size": data.len(),
975 "filename_resolved": resolved_filename.is_some()
976 });
977
978 if matches!(format, OutputFormat::JsonPretty) {
979 println!("{}", serde_json::to_string_pretty(&json)?);
980 } else {
981 println!("{}", serde_json::to_string(&json)?);
982 }
983 }
984 OutputFormat::Text => {
985 println!("โ
Extracted file successfully!");
986 println!(" EKey: {}", ekey.cyan());
987 println!(" Size: {} bytes", data.len().to_string().green());
988 println!(" Output: {:?}", output_path.bright_blue());
989
990 if resolved_filename.is_some() {
991 println!(" ๐ Filename resolved from listfile");
992 } else {
993 println!(" ๐ Used EKey as filename (no resolution available)");
994 }
995 }
996 OutputFormat::Bpsv => {
997 println!("status = success");
998 println!("ekey = {ekey}");
999 println!("output_path = {output_path:?}");
1000 println!("size = {}", data.len());
1001 println!("filename_resolved = {}", resolved_filename.is_some());
1002 }
1003 }
1004 }
1005
1006 Ok(())
1007}
1008
1009async fn handle_extract_by_id(
1010 fdid: u32,
1011 path: PathBuf,
1012 output: Option<PathBuf>,
1013 root_manifest: Option<PathBuf>,
1014 encoding_manifest: Option<PathBuf>,
1015 format: OutputFormat,
1016) -> Result<(), Box<dyn std::error::Error>> {
1017 let data_path = if path.ends_with("Data") {
1018 path.clone()
1019 } else {
1020 path.join("Data")
1021 };
1022
1023 let config = CascConfig {
1024 data_path: data_path.clone(),
1025 read_only: true,
1026 cache_size_mb: 256,
1027 max_archive_size: 1024 * 1024 * 1024,
1028 use_memory_mapping: true,
1029 };
1030
1031 let mut storage = CascStorage::new(config)?;
1032 storage.load_indices()?;
1033 storage.load_archives()?;
1034
1035 let manifest_config = ManifestConfig {
1037 locale: LocaleFlags::any_locale(),
1038 content_flags: None,
1039 cache_manifests: true,
1040 lazy_loading: true, lazy_cache_limit: 50_000, };
1043 storage.init_tact_manifests(manifest_config);
1044
1045 if let Some(root_path) = root_manifest {
1047 storage.load_root_manifest_from_file(&root_path)?;
1048 info!("Loaded root manifest from {:?}", root_path);
1049 }
1050
1051 if let Some(encoding_path) = encoding_manifest {
1052 storage.load_encoding_manifest_from_file(&encoding_path)?;
1053 info!("Loaded encoding manifest from {:?}", encoding_path);
1054 }
1055
1056 if !storage.tact_manifests_loaded() {
1057 return Err(
1058 "TACT manifests not loaded. Use --root-manifest and --encoding-manifest".into(),
1059 );
1060 }
1061
1062 debug!("Extracting FileDataID: {}", fdid);
1064 let data = storage.read_by_fdid(fdid)?;
1065
1066 let output_path = output.unwrap_or_else(|| PathBuf::from(format!("fdid_{fdid}.bin")));
1068
1069 if output_path.to_string_lossy() == "-" {
1071 io::stdout().write_all(&data)?;
1072 } else {
1073 if let Some(parent) = output_path.parent() {
1074 fs::create_dir_all(parent)?;
1075 }
1076 fs::write(&output_path, &data)?;
1077
1078 match format {
1079 OutputFormat::Json | OutputFormat::JsonPretty => {
1080 let json = serde_json::json!({
1081 "status": "success",
1082 "fdid": fdid,
1083 "output_path": output_path,
1084 "size": data.len()
1085 });
1086
1087 if matches!(format, OutputFormat::JsonPretty) {
1088 println!("{}", serde_json::to_string_pretty(&json)?);
1089 } else {
1090 println!("{}", serde_json::to_string(&json)?);
1091 }
1092 }
1093 OutputFormat::Text => {
1094 println!("โ
Extracted file successfully!");
1095 println!(" FileDataID: {}", fdid.to_string().cyan());
1096 println!(" Size: {} bytes", data.len().to_string().green());
1097 println!(" Output: {:?}", output_path.bright_blue());
1098 }
1099 OutputFormat::Bpsv => {
1100 println!("status = success");
1101 println!("fdid = {fdid}");
1102 println!("output_path = {output_path:?}");
1103 println!("size = {}", data.len());
1104 }
1105 }
1106 }
1107
1108 Ok(())
1109}
1110
1111async fn handle_extract_by_name(
1112 filename: String,
1113 path: PathBuf,
1114 output: Option<PathBuf>,
1115 root_manifest: Option<PathBuf>,
1116 encoding_manifest: Option<PathBuf>,
1117 listfile: Option<PathBuf>,
1118 format: OutputFormat,
1119) -> Result<(), Box<dyn std::error::Error>> {
1120 let data_path = if path.ends_with("Data") {
1121 path.clone()
1122 } else {
1123 path.join("Data")
1124 };
1125
1126 let config = CascConfig {
1127 data_path: data_path.clone(),
1128 read_only: true,
1129 cache_size_mb: 256,
1130 max_archive_size: 1024 * 1024 * 1024,
1131 use_memory_mapping: true,
1132 };
1133
1134 let mut storage = CascStorage::new(config)?;
1135 storage.load_indices()?;
1136 storage.load_archives()?;
1137
1138 let manifest_config = ManifestConfig {
1140 locale: LocaleFlags::any_locale(),
1141 content_flags: None,
1142 cache_manifests: true,
1143 lazy_loading: true, lazy_cache_limit: 50_000, };
1146 storage.init_tact_manifests(manifest_config);
1147
1148 if let Some(root_path) = root_manifest {
1150 storage.load_root_manifest_from_file(&root_path)?;
1151 info!("Loaded root manifest from {:?}", root_path);
1152 }
1153
1154 if let Some(encoding_path) = encoding_manifest {
1155 storage.load_encoding_manifest_from_file(&encoding_path)?;
1156 info!("Loaded encoding manifest from {:?}", encoding_path);
1157 }
1158
1159 if let Some(listfile_path) = listfile {
1161 let count = storage.load_listfile(&listfile_path)?;
1162 info!("Loaded {} filename mappings", count);
1163 }
1164
1165 if !storage.tact_manifests_loaded() {
1166 return Err(
1167 "TACT manifests not loaded. Use --root-manifest and --encoding-manifest".into(),
1168 );
1169 }
1170
1171 debug!("Extracting filename: {}", filename);
1173 let data = storage.read_by_filename(&filename)?;
1174
1175 let output_path = output.unwrap_or_else(|| {
1177 let safe_filename = filename.replace(['\\', '/', ':'], "_");
1179 PathBuf::from(safe_filename)
1180 });
1181
1182 if output_path.to_string_lossy() == "-" {
1184 io::stdout().write_all(&data)?;
1185 } else {
1186 if let Some(parent) = output_path.parent() {
1187 fs::create_dir_all(parent)?;
1188 }
1189 fs::write(&output_path, &data)?;
1190
1191 match format {
1192 OutputFormat::Json | OutputFormat::JsonPretty => {
1193 let json = serde_json::json!({
1194 "status": "success",
1195 "filename": filename,
1196 "output_path": output_path,
1197 "size": data.len()
1198 });
1199
1200 if matches!(format, OutputFormat::JsonPretty) {
1201 println!("{}", serde_json::to_string_pretty(&json)?);
1202 } else {
1203 println!("{}", serde_json::to_string(&json)?);
1204 }
1205 }
1206 OutputFormat::Text => {
1207 println!("โ
Extracted file successfully!");
1208 println!(" Filename: {}", filename.cyan());
1209 println!(" Size: {} bytes", data.len().to_string().green());
1210 println!(" Output: {:?}", output_path.bright_blue());
1211 }
1212 OutputFormat::Bpsv => {
1213 println!("status = success");
1214 println!("filename = {filename}");
1215 println!("output_path = {output_path:?}");
1216 println!("size = {}", data.len());
1217 }
1218 }
1219 }
1220
1221 Ok(())
1222}
1223
1224async fn handle_load_manifests(
1225 path: PathBuf,
1226 root_manifest: Option<PathBuf>,
1227 encoding_manifest: Option<PathBuf>,
1228 listfile: Option<PathBuf>,
1229 locale: String,
1230 info_only: bool,
1231 format: OutputFormat,
1232) -> Result<(), Box<dyn std::error::Error>> {
1233 let data_path = if path.ends_with("Data") {
1234 path.clone()
1235 } else {
1236 path.join("Data")
1237 };
1238
1239 let locale_flags = match locale.to_lowercase().as_str() {
1241 "all" => LocaleFlags::any_locale(),
1242 "en_us" => LocaleFlags::new().with_en_us(true),
1243 "de_de" => LocaleFlags::new().with_de_de(true),
1244 "fr_fr" => LocaleFlags::new().with_fr_fr(true),
1245 "es_es" => LocaleFlags::new().with_es_es(true),
1246 "zh_cn" => LocaleFlags::new().with_zh_cn(true),
1247 "zh_tw" => LocaleFlags::new().with_zh_tw(true),
1248 "ko_kr" => LocaleFlags::new().with_ko_kr(true),
1249 "ru_ru" => LocaleFlags::new().with_ru_ru(true),
1250 _ => {
1251 warn!("Unknown locale '{}', using 'all'", locale);
1252 LocaleFlags::any_locale()
1253 }
1254 };
1255
1256 let config = CascConfig {
1257 data_path: data_path.clone(),
1258 read_only: true,
1259 cache_size_mb: 256,
1260 max_archive_size: 1024 * 1024 * 1024,
1261 use_memory_mapping: true,
1262 };
1263
1264 let mut storage = CascStorage::new(config)?;
1265 storage.load_indices()?;
1266 storage.load_archives()?;
1267
1268 let manifest_config = ManifestConfig {
1270 locale: locale_flags,
1271 content_flags: None,
1272 cache_manifests: true,
1273 lazy_loading: true, lazy_cache_limit: 50_000, };
1276 storage.init_tact_manifests(manifest_config);
1277
1278 let mut stats = serde_json::json!({
1279 "manifests_loaded": {},
1280 "errors": []
1281 });
1282
1283 if let Some(root_path) = root_manifest {
1285 match storage.load_root_manifest_from_file(&root_path) {
1286 Ok(_) => {
1287 info!("Successfully loaded root manifest from {:?}", root_path);
1288 stats["manifests_loaded"]["root"] = serde_json::json!({
1289 "path": root_path,
1290 "status": "success"
1291 });
1292 }
1293 Err(e) => {
1294 error!("Failed to load root manifest: {}", e);
1295 stats["errors"]
1296 .as_array_mut()
1297 .unwrap()
1298 .push(serde_json::json!({
1299 "manifest": "root",
1300 "path": root_path,
1301 "error": e.to_string()
1302 }));
1303 }
1304 }
1305 }
1306
1307 if let Some(encoding_path) = encoding_manifest {
1309 match storage.load_encoding_manifest_from_file(&encoding_path) {
1310 Ok(_) => {
1311 info!(
1312 "Successfully loaded encoding manifest from {:?}",
1313 encoding_path
1314 );
1315 stats["manifests_loaded"]["encoding"] = serde_json::json!({
1316 "path": encoding_path,
1317 "status": "success"
1318 });
1319 }
1320 Err(e) => {
1321 error!("Failed to load encoding manifest: {}", e);
1322 stats["errors"]
1323 .as_array_mut()
1324 .unwrap()
1325 .push(serde_json::json!({
1326 "manifest": "encoding",
1327 "path": encoding_path,
1328 "error": e.to_string()
1329 }));
1330 }
1331 }
1332 }
1333
1334 if let Some(listfile_path) = listfile {
1336 match storage.load_listfile(&listfile_path) {
1337 Ok(count) => {
1338 info!(
1339 "Successfully loaded {} filename mappings from listfile",
1340 count
1341 );
1342 stats["manifests_loaded"]["listfile"] = serde_json::json!({
1343 "path": listfile_path,
1344 "status": "success",
1345 "entries": count
1346 });
1347 }
1348 Err(e) => {
1349 error!("Failed to load listfile: {}", e);
1350 stats["errors"]
1351 .as_array_mut()
1352 .unwrap()
1353 .push(serde_json::json!({
1354 "manifest": "listfile",
1355 "path": listfile_path,
1356 "error": e.to_string()
1357 }));
1358 }
1359 }
1360 }
1361
1362 if storage.tact_manifests_loaded() {
1364 if let Ok(fdids) = storage.get_all_fdids() {
1365 stats["file_count"] = fdids.len().into();
1366 }
1367 }
1368
1369 match format {
1370 OutputFormat::Json | OutputFormat::JsonPretty => {
1371 if matches!(format, OutputFormat::JsonPretty) {
1372 println!("{}", serde_json::to_string_pretty(&stats)?);
1373 } else {
1374 println!("{}", serde_json::to_string(&stats)?);
1375 }
1376 }
1377 OutputFormat::Text => {
1378 println!("๐ TACT Manifest Loading Results");
1379 println!("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
1380
1381 if storage.tact_manifests_loaded() {
1382 println!("โ
TACT manifests loaded successfully");
1383
1384 if let Ok(fdids) = storage.get_all_fdids() {
1385 println!(
1386 " FileDataIDs available: {}",
1387 fdids.len().to_string().green()
1388 );
1389 }
1390
1391 println!(" Locale filter: {}", locale.yellow());
1392
1393 if info_only {
1394 println!(" โน๏ธ Info-only mode (not persisted)");
1395 }
1396 } else {
1397 println!("โ TACT manifests not fully loaded");
1398 }
1399
1400 if !stats["errors"].as_array().unwrap().is_empty() {
1401 println!("\nโ ๏ธ Errors:");
1402 for error in stats["errors"].as_array().unwrap() {
1403 println!(
1404 " โข {}: {}",
1405 error["manifest"].as_str().unwrap(),
1406 error["error"].as_str().unwrap()
1407 );
1408 }
1409 }
1410 }
1411 OutputFormat::Bpsv => {
1412 println!("## TACT Manifests");
1413 println!("loaded = {}", storage.tact_manifests_loaded());
1414 if let Ok(fdids) = storage.get_all_fdids() {
1415 println!("file_count = {}", fdids.len());
1416 }
1417 println!("locale = {locale}");
1418 println!("errors = {}", stats["errors"].as_array().unwrap().len());
1419 }
1420 }
1421
1422 Ok(())
1423}
1424
1425fn format_bytes(bytes: u64) -> String {
1426 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
1427 let mut size = bytes as f64;
1428 let mut unit = 0;
1429
1430 while size >= 1024.0 && unit < UNITS.len() - 1 {
1431 size /= 1024.0;
1432 unit += 1;
1433 }
1434
1435 if unit == 0 {
1436 format!("{} {}", bytes, UNITS[unit])
1437 } else {
1438 format!("{:.2} {}", size, UNITS[unit])
1439 }
1440}