casc_storage/
config.rs

1//! Configuration discovery and parsing for WoW installations
2//!
3//! This module discovers and parses NGDP configuration files stored in
4//! WoW installations under the `Data/config/` directory.
5
6use crate::error::{CascError, Result};
7use std::fs;
8use std::path::{Path, PathBuf};
9use tact_parser::config::{BuildConfig, CdnConfig, ConfigFile};
10use tracing::{debug, trace};
11
12/// Discovered configuration files for a WoW installation
13#[derive(Debug, Clone)]
14pub struct WowConfigSet {
15    /// All discovered CDN configs
16    pub cdn_configs: Vec<CdnConfig>,
17
18    /// All discovered build configs
19    pub build_configs: Vec<BuildConfig>,
20
21    /// Directory where configs were found
22    pub config_dir: PathBuf,
23}
24
25impl WowConfigSet {
26    /// Get the most recent CDN config (if any)
27    pub fn latest_cdn_config(&self) -> Option<&CdnConfig> {
28        self.cdn_configs.first()
29    }
30
31    /// Get the most recent build config (if any)
32    pub fn latest_build_config(&self) -> Option<&BuildConfig> {
33        self.build_configs.first()
34    }
35
36    /// Get all archive hashes from CDN configs
37    pub fn all_archive_hashes(&self) -> Vec<String> {
38        let mut hashes = Vec::new();
39        for cdn_config in &self.cdn_configs {
40            hashes.extend(cdn_config.archives().iter().map(|s| s.to_string()));
41        }
42        hashes.sort();
43        hashes.dedup();
44        hashes
45    }
46
47    /// Get file index hashes
48    pub fn file_index_hashes(&self) -> Vec<String> {
49        let mut hashes = Vec::new();
50        for cdn_config in &self.cdn_configs {
51            if let Some(file_index) = cdn_config.file_index() {
52                hashes.push(file_index.to_string());
53            }
54        }
55        hashes.sort();
56        hashes.dedup();
57        hashes
58    }
59}
60
61/// Configuration discovery for WoW installations
62pub struct ConfigDiscovery;
63
64impl ConfigDiscovery {
65    /// Discover all configuration files in a WoW installation
66    pub fn discover_configs<P: AsRef<Path>>(wow_path: P) -> Result<WowConfigSet> {
67        let wow_path = wow_path.as_ref();
68
69        // Check for Data/config directory
70        let config_dir = Self::find_config_directory(wow_path)?;
71        debug!("Found config directory: {:?}", config_dir);
72
73        let mut cdn_configs = Vec::new();
74        let mut build_configs = Vec::new();
75
76        // Scan all config files
77        let config_files = Self::scan_config_files(&config_dir)?;
78        debug!("Found {} config files", config_files.len());
79
80        for config_path in config_files {
81            match Self::parse_config_file(&config_path)? {
82                ConfigType::Cdn(cdn_config) => {
83                    trace!("Found CDN config: {:?}", config_path.file_name());
84                    cdn_configs.push(cdn_config);
85                }
86                ConfigType::Build(build_config) => {
87                    trace!("Found build config: {:?}", config_path.file_name());
88                    build_configs.push(build_config);
89                }
90                ConfigType::Unknown => {
91                    trace!("Unknown config type: {:?}", config_path.file_name());
92                }
93            }
94        }
95
96        debug!(
97            "Discovered {} CDN configs, {} build configs",
98            cdn_configs.len(),
99            build_configs.len()
100        );
101
102        Ok(WowConfigSet {
103            cdn_configs,
104            build_configs,
105            config_dir,
106        })
107    }
108
109    /// Find the config directory in a WoW installation
110    fn find_config_directory<P: AsRef<Path>>(wow_path: P) -> Result<PathBuf> {
111        let wow_path = wow_path.as_ref();
112
113        // Try Data/config first
114        let data_config = wow_path.join("Data").join("config");
115        if data_config.exists() && data_config.is_dir() {
116            return Ok(data_config);
117        }
118
119        // Try just config if wow_path ends with Data
120        let config_dir = wow_path.join("config");
121        if config_dir.exists() && config_dir.is_dir() {
122            return Ok(config_dir);
123        }
124
125        Err(CascError::InvalidIndexFormat(format!(
126            "No config directory found in WoW installation: {wow_path:?}"
127        )))
128    }
129
130    /// Scan for all config files in the config directory
131    fn scan_config_files(config_dir: &Path) -> Result<Vec<PathBuf>> {
132        let mut config_files = Vec::new();
133
134        // Config files are stored in hash-based subdirectories like ab/cd/abcd1234...
135        for entry in fs::read_dir(config_dir)? {
136            let entry = entry?;
137            let path = entry.path();
138
139            if path.is_dir() {
140                // Check subdirectories for config files
141                if let Ok(subentries) = fs::read_dir(&path) {
142                    for subentry in subentries {
143                        let subentry = subentry?;
144                        let subpath = subentry.path();
145
146                        if subpath.is_dir() {
147                            // Check files in the hash subdirectory
148                            if let Ok(files) = fs::read_dir(&subpath) {
149                                for file in files {
150                                    let file = file?;
151                                    let file_path = file.path();
152
153                                    if file_path.is_file() {
154                                        config_files.push(file_path);
155                                    }
156                                }
157                            }
158                        }
159                    }
160                }
161            }
162        }
163
164        trace!("Scanned config files: {:?}", config_files);
165        Ok(config_files)
166    }
167
168    /// Parse a config file and determine its type
169    fn parse_config_file(path: &Path) -> Result<ConfigType> {
170        let content = fs::read_to_string(path).map_err(CascError::Io)?;
171
172        // Skip empty files
173        if content.trim().is_empty() {
174            return Ok(ConfigType::Unknown);
175        }
176
177        // Parse as generic config first
178        let config = ConfigFile::parse(&content)
179            .map_err(|e| CascError::InvalidIndexFormat(format!("Config parse error: {e}")))?;
180
181        // Determine type based on keys present
182        if Self::is_cdn_config(&config) {
183            let cdn_config = CdnConfig::parse(&content).map_err(|e| {
184                CascError::InvalidIndexFormat(format!("CDN config parse error: {e}"))
185            })?;
186            Ok(ConfigType::Cdn(cdn_config))
187        } else if Self::is_build_config(&config) {
188            let build_config = BuildConfig::parse(&content).map_err(|e| {
189                CascError::InvalidIndexFormat(format!("Build config parse error: {e}"))
190            })?;
191            Ok(ConfigType::Build(build_config))
192        } else {
193            Ok(ConfigType::Unknown)
194        }
195    }
196
197    /// Check if a config file is a CDN config based on its keys
198    fn is_cdn_config(config: &ConfigFile) -> bool {
199        // CDN configs typically have archives, file-index, etc.
200        config.has_key("archives")
201            || config.has_key("archive-group")
202            || config.has_key("file-index")
203    }
204
205    /// Check if a config file is a build config based on its keys
206    fn is_build_config(config: &ConfigFile) -> bool {
207        // Build configs typically have root, encoding, install, etc.
208        config.has_key("root")
209            || config.has_key("encoding")
210            || config.has_key("install")
211            || config.has_key("build-name")
212    }
213}
214
215/// Type of configuration file
216#[derive(Debug)]
217enum ConfigType {
218    /// CDN configuration
219    Cdn(CdnConfig),
220    /// Build configuration
221    Build(BuildConfig),
222    /// Unknown or unsupported type
223    Unknown,
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use std::fs;
230    use tempfile::TempDir;
231
232    fn create_test_config_structure() -> TempDir {
233        let temp_dir = TempDir::new().unwrap();
234        let config_dir = temp_dir.path().join("Data").join("config");
235
236        // Create the directory structure: config/ab/cd/abcd1234...
237        let hash_dir = config_dir.join("ab").join("cd");
238        fs::create_dir_all(&hash_dir).unwrap();
239
240        // Create a CDN config file
241        let cdn_config_content = r#"# CDN Configuration
242archives = abc123 def456 789abc
243archive-group = group123
244file-index = index456
245"#;
246        fs::write(hash_dir.join("abcd1234567890abcdef"), cdn_config_content).unwrap();
247
248        // Create another directory for build config
249        let build_hash_dir = config_dir.join("12").join("34");
250        fs::create_dir_all(&build_hash_dir).unwrap();
251
252        // Create a build config file
253        let build_config_content = r#"# Build Configuration
254root = abc123def456 100
255encoding = 789abcdef012 200
256install = fedcba987654 300
257build-name = 1.13.2.31650
258"#;
259        fs::write(
260            build_hash_dir.join("1234567890abcdef1234"),
261            build_config_content,
262        )
263        .unwrap();
264
265        temp_dir
266    }
267
268    #[test]
269    fn test_discover_configs() {
270        let temp_dir = create_test_config_structure();
271        let config_set = ConfigDiscovery::discover_configs(temp_dir.path()).unwrap();
272
273        assert_eq!(config_set.cdn_configs.len(), 1);
274        assert_eq!(config_set.build_configs.len(), 1);
275
276        let cdn_config = config_set.latest_cdn_config().unwrap();
277        let archives = cdn_config.archives();
278        assert_eq!(archives.len(), 3);
279        assert_eq!(archives[0], "abc123");
280
281        let build_config = config_set.latest_build_config().unwrap();
282        assert_eq!(build_config.build_name(), Some("1.13.2.31650"));
283    }
284
285    #[test]
286    fn test_config_type_detection() {
287        let cdn_content = "archives = abc def\nfile-index = 123\n";
288        let cdn_config = ConfigFile::parse(cdn_content).unwrap();
289        assert!(ConfigDiscovery::is_cdn_config(&cdn_config));
290        assert!(!ConfigDiscovery::is_build_config(&cdn_config));
291
292        let build_content = "root = abc123 100\nencoding = def456 200\n";
293        let build_config = ConfigFile::parse(build_content).unwrap();
294        assert!(!ConfigDiscovery::is_cdn_config(&build_config));
295        assert!(ConfigDiscovery::is_build_config(&build_config));
296    }
297}