ngdp_client/lib.rs
1//! NGDP client library
2//!
3//! This library provides the core functionality for the ngdp CLI tool.
4
5pub mod cached_client;
6pub mod cdn_config;
7pub mod commands;
8pub mod config_manager;
9pub mod fallback_client;
10pub mod manifest;
11pub mod output;
12pub mod pattern_extraction;
13pub mod wago_api;
14
15/// Common test constants
16pub mod test_constants {
17 /// Example certificate hash used throughout tests and examples
18 /// SKI: 5168ff90af0207753cccd9656462a212b859723b
19 pub const EXAMPLE_CERT_HASH: &str = "5168ff90af0207753cccd9656462a212b859723b";
20}
21
22// Re-export command handlers
23pub use crate::commands::{
24 certs::handle as handle_certs, config::handle as handle_config,
25 download::handle as handle_download, inspect::handle as handle_inspect,
26 install::handle as handle_install, listfile::handle as handle_listfile,
27 products::handle as handle_products, storage::handle as handle_storage,
28};
29
30use clap::Subcommand;
31use std::path::PathBuf;
32
33#[derive(Subcommand)]
34pub enum ProductsCommands {
35 /// List all available products
36 List {
37 /// Filter by product name pattern
38 #[arg(short, long)]
39 filter: Option<String>,
40
41 /// Region to query
42 #[arg(short, long, default_value = "us")]
43 region: String,
44 },
45
46 /// Show versions for a specific product
47 Versions {
48 /// Product name (e.g., wow, d3, agent)
49 product: String,
50
51 /// Region to query
52 #[arg(short, long, default_value = "us")]
53 region: String,
54
55 /// Show all regions
56 #[arg(short, long)]
57 all_regions: bool,
58
59 /// Parse and show build configuration details
60 #[arg(long)]
61 parse_config: bool,
62 },
63
64 /// Show CDN configuration for a product
65 Cdns {
66 /// Product name
67 product: String,
68
69 /// Region to query
70 #[arg(short, long, default_value = "us")]
71 region: String,
72 },
73
74 /// Get detailed information about a product
75 Info {
76 /// Product name
77 product: String,
78
79 /// Region to query (omit to show all regions)
80 #[arg(short, long)]
81 region: Option<String>,
82 },
83
84 /// Show all historical builds for a product
85 Builds {
86 /// Product name (e.g., wow, wowt, wowxptr)
87 product: String,
88
89 /// Filter by version pattern
90 #[arg(short, long)]
91 filter: Option<String>,
92
93 /// Show only builds from the last N days
94 #[arg(long)]
95 days: Option<u32>,
96
97 /// Limit number of results (default: show all)
98 #[arg(long)]
99 limit: Option<usize>,
100
101 /// Show only background download builds
102 #[arg(long)]
103 bgdl_only: bool,
104 },
105}
106
107#[derive(Subcommand)]
108pub enum StorageCommands {
109 /// Initialize a new CASC storage
110 Init {
111 /// Path to storage directory
112 #[arg(default_value = ".")]
113 path: PathBuf,
114
115 /// Product to initialize for
116 #[arg(short, long)]
117 product: Option<String>,
118 },
119
120 /// Show storage information
121 Info {
122 /// Path to storage directory
123 #[arg(default_value = ".")]
124 path: PathBuf,
125 },
126
127 /// Show NGDP configuration information from WoW installation
128 Config {
129 /// Path to WoW installation directory
130 #[arg(default_value = ".")]
131 path: PathBuf,
132 },
133
134 /// Show detailed storage statistics
135 Stats {
136 /// Path to storage directory
137 #[arg(default_value = ".")]
138 path: PathBuf,
139 },
140
141 /// Verify storage integrity
142 Verify {
143 /// Path to storage directory
144 #[arg(default_value = ".")]
145 path: PathBuf,
146
147 /// Fix corrupted files
148 #[arg(short, long)]
149 fix: bool,
150 },
151
152 /// Read a file by EKey
153 Read {
154 /// Path to storage directory
155 path: PathBuf,
156
157 /// Encoding key (hex)
158 ekey: String,
159
160 /// Output file (defaults to stdout)
161 #[arg(short = 'O', long)]
162 output: Option<PathBuf>,
163 },
164
165 /// Write a file to storage
166 Write {
167 /// Path to storage directory
168 path: PathBuf,
169
170 /// Encoding key (hex)
171 ekey: String,
172
173 /// Input file (defaults to stdin)
174 #[arg(short = 'I', long)]
175 input: Option<PathBuf>,
176 },
177
178 /// List all files in storage
179 List {
180 /// Path to storage directory
181 #[arg(default_value = ".")]
182 path: PathBuf,
183
184 /// Show detailed information
185 #[arg(short, long)]
186 detailed: bool,
187
188 /// Limit number of results
189 #[arg(short = 'n', long)]
190 limit: Option<usize>,
191 },
192
193 /// Rebuild storage indices
194 Rebuild {
195 /// Path to storage directory
196 #[arg(default_value = ".")]
197 path: PathBuf,
198
199 /// Force rebuild even if indices seem valid
200 #[arg(short, long)]
201 force: bool,
202 },
203
204 /// Optimize storage for performance
205 Optimize {
206 /// Path to storage directory
207 #[arg(default_value = ".")]
208 path: PathBuf,
209 },
210
211 /// Repair corrupted storage
212 Repair {
213 /// Path to storage directory
214 #[arg(default_value = ".")]
215 path: PathBuf,
216
217 /// Dry run (don't actually repair)
218 #[arg(short = 'n', long)]
219 dry_run: bool,
220 },
221
222 /// Clean up unused data
223 Clean {
224 /// Path to storage directory
225 #[arg(default_value = ".")]
226 path: PathBuf,
227
228 /// Dry run (don't actually delete)
229 #[arg(short = 'n', long)]
230 dry_run: bool,
231 },
232
233 /// Extract a file by EKey with optional filename resolution
234 Extract {
235 /// Encoding key (hex)
236 ekey: String,
237
238 /// Path to storage directory
239 #[arg(long, default_value = ".")]
240 path: PathBuf,
241
242 /// Output file path (optional)
243 #[arg(short = 'O', long)]
244 output: Option<PathBuf>,
245
246 /// Path to community listfile for filename resolution
247 #[arg(long)]
248 listfile: Option<PathBuf>,
249
250 /// Resolve filename using listfile and TACT manifests
251 #[arg(long)]
252 resolve_filename: bool,
253 },
254
255 /// Extract a file by FileDataID (requires TACT manifests)
256 ExtractById {
257 /// FileDataID to extract
258 fdid: u32,
259
260 /// Path to storage directory
261 #[arg(long, default_value = ".")]
262 path: PathBuf,
263
264 /// Output file path (optional)
265 #[arg(short = 'O', long)]
266 output: Option<PathBuf>,
267
268 /// Path to root manifest file
269 #[arg(long)]
270 root_manifest: Option<PathBuf>,
271
272 /// Path to encoding manifest file
273 #[arg(long)]
274 encoding_manifest: Option<PathBuf>,
275 },
276
277 /// Extract a file by filename (requires TACT manifests and listfile)
278 ExtractByName {
279 /// Filename to extract
280 filename: String,
281
282 /// Path to storage directory
283 #[arg(long, default_value = ".")]
284 path: PathBuf,
285
286 /// Output file path (optional)
287 #[arg(short = 'O', long)]
288 output: Option<PathBuf>,
289
290 /// Path to root manifest file
291 #[arg(long)]
292 root_manifest: Option<PathBuf>,
293
294 /// Path to encoding manifest file
295 #[arg(long)]
296 encoding_manifest: Option<PathBuf>,
297
298 /// Path to community listfile for filename resolution
299 #[arg(long)]
300 listfile: Option<PathBuf>,
301 },
302
303 /// Load TACT manifests for enhanced operations
304 LoadManifests {
305 /// Path to storage directory
306 #[arg(default_value = ".")]
307 path: PathBuf,
308
309 /// Path to root manifest file
310 #[arg(long)]
311 root_manifest: Option<PathBuf>,
312
313 /// Path to encoding manifest file
314 #[arg(long)]
315 encoding_manifest: Option<PathBuf>,
316
317 /// Path to community listfile for filename resolution
318 #[arg(long)]
319 listfile: Option<PathBuf>,
320
321 /// Locale to use for filtering (default: all)
322 #[arg(long, default_value = "all")]
323 locale: String,
324
325 /// Only show info, don't persist
326 #[arg(long)]
327 info_only: bool,
328 },
329}
330
331#[derive(Subcommand)]
332pub enum ListfileCommands {
333 /// Download the latest community listfile
334 Download {
335 /// Output directory for listfile
336 #[arg(long, default_value = ".")]
337 output: PathBuf,
338
339 /// Force download even if file exists
340 #[arg(short, long)]
341 force: bool,
342 },
343
344 /// Show listfile information
345 Info {
346 /// Path to listfile
347 #[arg(default_value = "community-listfile.csv")]
348 path: PathBuf,
349 },
350
351 /// Search for files in listfile
352 Search {
353 /// Search pattern (regex)
354 pattern: String,
355
356 /// Path to listfile
357 #[arg(default_value = "community-listfile.csv")]
358 path: PathBuf,
359
360 /// Case-insensitive search
361 #[arg(short, long)]
362 ignore_case: bool,
363
364 /// Limit results
365 #[arg(short, long, default_value = "50")]
366 limit: usize,
367 },
368}
369
370#[derive(Subcommand)]
371pub enum DownloadCommands {
372 /// Download a specific build
373 Build {
374 /// Product name
375 product: String,
376
377 /// Build ID or version
378 build: String,
379
380 /// Output directory
381 #[arg(long, default_value = ".")]
382 output: PathBuf,
383
384 /// Region
385 #[arg(short, long, default_value = "us")]
386 region: String,
387
388 /// Dry run - show what would be downloaded without actually downloading
389 #[arg(long)]
390 dry_run: bool,
391
392 /// Filter by tags (comma-separated)
393 #[arg(long)]
394 tags: Option<String>,
395 },
396
397 /// Download specific files
398 Files {
399 /// Product name
400 product: String,
401
402 /// File patterns to download
403 patterns: Vec<String>,
404
405 /// Output directory
406 #[arg(long, default_value = ".")]
407 output: PathBuf,
408
409 /// Build ID or version
410 #[arg(short, long)]
411 build: Option<String>,
412
413 /// Dry run - show what would be downloaded without actually downloading
414 #[arg(long)]
415 dry_run: bool,
416
417 /// Filter by tags (comma-separated)
418 #[arg(long)]
419 tags: Option<String>,
420
421 /// Limit number of files to download
422 #[arg(long)]
423 limit: Option<usize>,
424 },
425
426 /// Resume an interrupted download
427 Resume {
428 /// Session ID or path
429 session: String,
430 },
431
432 /// Test resumable download with a known file (for testing)
433 TestResume {
434 /// File hash to download (32 hex chars)
435 hash: String,
436
437 /// CDN host
438 #[arg(short = 'H', long, default_value = "blzddist1-a.akamaihd.net")]
439 host: String,
440
441 /// Output file path
442 #[arg(long, default_value = "test_download.bin")]
443 output: PathBuf,
444
445 /// Enable resumable mode
446 #[arg(short, long)]
447 resumable: bool,
448 },
449}
450
451#[derive(Subcommand)]
452pub enum InstallCommands {
453 /// Install a game or product
454 Game {
455 /// Product name (e.g., wow, wow_classic)
456 product: String,
457
458 /// Installation directory
459 #[arg(long, default_value = ".")]
460 path: PathBuf,
461
462 /// Specific build to install (defaults to latest)
463 #[arg(short, long)]
464 build: Option<String>,
465
466 /// Region
467 #[arg(short, long, default_value = "us")]
468 region: String,
469
470 /// Installation type
471 #[arg(short = 't', long, value_enum, default_value = "minimal")]
472 install_type: InstallType,
473
474 /// Resume existing installation (detects .build.info and missing files)
475 #[arg(long)]
476 resume: bool,
477
478 /// Verify installation after completion
479 #[arg(short = 'v', long)]
480 verify: bool,
481
482 /// Dry run - show what would be installed without downloading
483 #[arg(long)]
484 dry_run: bool,
485
486 /// Maximum concurrent downloads
487 #[arg(long, default_value = "5")]
488 max_concurrent: usize,
489
490 /// Filter by tags (comma-separated, e.g., "Windows,enUS")
491 #[arg(long)]
492 tags: Option<String>,
493 },
494
495 /// Repair an existing installation by verifying and re-downloading corrupted files
496 Repair {
497 /// Installation directory containing .build.info
498 #[arg(long, default_value = ".")]
499 path: PathBuf,
500
501 /// Verify checksums of existing files
502 #[arg(short = 'v', long)]
503 verify_checksums: bool,
504
505 /// Dry run - show what would be repaired without downloading
506 #[arg(long)]
507 dry_run: bool,
508
509 /// Maximum concurrent downloads
510 #[arg(long, default_value = "5")]
511 max_concurrent: usize,
512 },
513}
514
515#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
516pub enum InstallType {
517 /// Only required files for basic functionality
518 Minimal,
519 /// All available content
520 Full,
521 /// Custom selection based on tags
522 Custom,
523 /// Only create .build.info and Data/config structure (no downloads)
524 MetadataOnly,
525}
526
527#[derive(Subcommand)]
528pub enum InspectCommands {
529 /// Parse and display BPSV data
530 Bpsv {
531 /// Input file or URL
532 input: String,
533
534 /// Show raw data
535 #[arg(short, long)]
536 raw: bool,
537 },
538
539 /// Inspect build configuration
540 BuildConfig {
541 /// Product name
542 product: String,
543
544 /// Build ID
545 build: String,
546
547 /// Region
548 #[arg(short, long, default_value = "us")]
549 region: String,
550 },
551
552 /// Inspect CDN configuration
553 CdnConfig {
554 /// Product name
555 product: String,
556
557 /// Region
558 #[arg(short, long, default_value = "us")]
559 region: String,
560 },
561
562 /// Inspect encoding file
563 Encoding {
564 /// Product name
565 product: String,
566
567 /// Region
568 #[arg(short, long, default_value = "us")]
569 region: String,
570
571 /// Show statistics
572 #[arg(short, long)]
573 stats: bool,
574
575 /// Search for specific key (hex string)
576 #[arg(long)]
577 search: Option<String>,
578
579 /// Limit number of entries shown
580 #[arg(long, default_value = "20")]
581 limit: usize,
582 },
583
584 /// Inspect install manifest
585 Install {
586 /// Product name
587 product: String,
588
589 /// Region
590 #[arg(short, long, default_value = "us")]
591 region: String,
592
593 /// Filter by tags (comma-separated)
594 #[arg(long)]
595 tags: Option<String>,
596
597 /// Show all entries (not just summary)
598 #[arg(long)]
599 all: bool,
600 },
601
602 /// Inspect download manifest
603 DownloadManifest {
604 /// Product name
605 product: String,
606
607 /// Region
608 #[arg(short, long, default_value = "us")]
609 region: String,
610
611 /// Show priority files
612 #[arg(long, default_value = "10")]
613 priority_limit: usize,
614
615 /// Filter by tags (comma-separated)
616 #[arg(long)]
617 tags: Option<String>,
618 },
619
620 /// Inspect size file
621 Size {
622 /// Product name
623 product: String,
624
625 /// Region
626 #[arg(short, long, default_value = "us")]
627 region: String,
628
629 /// Show largest files
630 #[arg(long, default_value = "10")]
631 largest: usize,
632
633 /// Calculate size for tags (comma-separated)
634 #[arg(long)]
635 tags: Option<String>,
636 },
637}
638
639#[derive(Subcommand)]
640pub enum ConfigCommands {
641 /// Show current configuration
642 Show,
643
644 /// Set a configuration value
645 Set {
646 /// Configuration key
647 key: String,
648
649 /// Configuration value
650 value: String,
651 },
652
653 /// Get a configuration value
654 Get {
655 /// Configuration key
656 key: String,
657 },
658
659 /// Reset configuration to defaults
660 Reset {
661 /// Confirm reset
662 #[arg(short, long)]
663 yes: bool,
664 },
665}
666
667#[derive(Subcommand)]
668pub enum CertsCommands {
669 /// Download a certificate by its SKI/hash
670 Download {
671 /// Subject Key Identifier or certificate hash
672 ski: String,
673
674 /// Output file (defaults to stdout)
675 #[arg(long)]
676 output: Option<PathBuf>,
677
678 /// Region to query
679 #[arg(short, long, default_value = "us")]
680 region: String,
681
682 /// Certificate format (pem or der)
683 #[arg(short = 'F', long = "cert-format", value_enum, default_value = "pem")]
684 cert_format: CertFormat,
685
686 /// Show certificate details
687 #[arg(short, long)]
688 details: bool,
689 },
690}
691
692#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
693pub enum CertFormat {
694 /// PEM format (text)
695 Pem,
696 /// DER format (binary)
697 Der,
698}
699
700/// Output format options for the CLI
701#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
702pub enum OutputFormat {
703 /// Plain text output
704 Text,
705 /// JSON output
706 Json,
707 /// Pretty-printed JSON
708 JsonPretty,
709 /// Raw BPSV format
710 Bpsv,
711}
712
713/// Context for command execution
714#[derive(Clone, Debug)]
715pub struct CommandContext {
716 /// Output format
717 pub format: OutputFormat,
718 /// Whether to disable colors
719 pub no_color: bool,
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 #[test]
727 fn test_output_format_debug() {
728 assert_eq!(format!("{:?}", OutputFormat::Text), "Text");
729 assert_eq!(format!("{:?}", OutputFormat::Json), "Json");
730 assert_eq!(format!("{:?}", OutputFormat::JsonPretty), "JsonPretty");
731 assert_eq!(format!("{:?}", OutputFormat::Bpsv), "Bpsv");
732 }
733}