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}