1use 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#[derive(Debug, Clone)]
14pub struct WowConfigSet {
15 pub cdn_configs: Vec<CdnConfig>,
17
18 pub build_configs: Vec<BuildConfig>,
20
21 pub config_dir: PathBuf,
23}
24
25impl WowConfigSet {
26 pub fn latest_cdn_config(&self) -> Option<&CdnConfig> {
28 self.cdn_configs.first()
29 }
30
31 pub fn latest_build_config(&self) -> Option<&BuildConfig> {
33 self.build_configs.first()
34 }
35
36 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 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
61pub struct ConfigDiscovery;
63
64impl ConfigDiscovery {
65 pub fn discover_configs<P: AsRef<Path>>(wow_path: P) -> Result<WowConfigSet> {
67 let wow_path = wow_path.as_ref();
68
69 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 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 fn find_config_directory<P: AsRef<Path>>(wow_path: P) -> Result<PathBuf> {
111 let wow_path = wow_path.as_ref();
112
113 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 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 fn scan_config_files(config_dir: &Path) -> Result<Vec<PathBuf>> {
132 let mut config_files = Vec::new();
133
134 for entry in fs::read_dir(config_dir)? {
136 let entry = entry?;
137 let path = entry.path();
138
139 if path.is_dir() {
140 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 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 fn parse_config_file(path: &Path) -> Result<ConfigType> {
170 let content = fs::read_to_string(path).map_err(CascError::Io)?;
171
172 if content.trim().is_empty() {
174 return Ok(ConfigType::Unknown);
175 }
176
177 let config = ConfigFile::parse(&content)
179 .map_err(|e| CascError::InvalidIndexFormat(format!("Config parse error: {e}")))?;
180
181 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 fn is_cdn_config(config: &ConfigFile) -> bool {
199 config.has_key("archives")
201 || config.has_key("archive-group")
202 || config.has_key("file-index")
203 }
204
205 fn is_build_config(config: &ConfigFile) -> bool {
207 config.has_key("root")
209 || config.has_key("encoding")
210 || config.has_key("install")
211 || config.has_key("build-name")
212 }
213}
214
215#[derive(Debug)]
217enum ConfigType {
218 Cdn(CdnConfig),
220 Build(BuildConfig),
222 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 let hash_dir = config_dir.join("ab").join("cd");
238 fs::create_dir_all(&hash_dir).unwrap();
239
240 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 let build_hash_dir = config_dir.join("12").join("34");
250 fs::create_dir_all(&build_hash_dir).unwrap();
251
252 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}