1use std::collections::HashMap;
7use tracing::{debug, trace};
8
9use crate::Result;
10
11#[derive(Debug, Clone, PartialEq)]
13pub struct HashPair {
14 pub hash: String,
16 pub size: u64,
18}
19
20#[derive(Debug, Clone)]
22pub struct ConfigFile {
23 pub values: HashMap<String, String>,
25 pub hashes: HashMap<String, HashPair>,
27}
28
29impl ConfigFile {
30 pub fn parse(text: &str) -> Result<Self> {
32 let mut values = HashMap::new();
33 let mut hashes = HashMap::new();
34
35 for line in text.lines() {
36 let line = line.trim();
37
38 if line.is_empty() || line.starts_with('#') {
40 continue;
41 }
42
43 let (key, value) = if let Some(eq_pos) = line.find(" = ") {
45 let key = line[..eq_pos].trim();
46 let value = line[eq_pos + 3..].trim(); (key, value)
48 } else if let Some(eq_pos) = line.find(" =") {
49 let key = line[..eq_pos].trim();
50 let value = line[eq_pos + 2..].trim(); (key, value)
52 } else {
53 continue; };
55
56 trace!("Config entry: '{}' = '{}'", key, value);
57
58 values.insert(key.to_string(), value.to_string());
60
61 if !value.is_empty() {
63 let parts: Vec<&str> = value.split_whitespace().collect();
64 if !parts.is_empty() && is_hex_hash(parts[0]) {
65 if parts.len() >= 2 {
67 if let Ok(size) = parts[1].parse::<u64>() {
68 hashes.insert(
69 key.to_string(),
70 HashPair {
71 hash: parts[0].to_string(),
72 size,
73 },
74 );
75 }
76 }
77 }
80 }
81 }
82
83 debug!(
84 "Parsed config with {} entries, {} hash pairs",
85 values.len(),
86 hashes.len()
87 );
88
89 Ok(ConfigFile { values, hashes })
90 }
91
92 pub fn get_value(&self, key: &str) -> Option<&str> {
94 self.values.get(key).map(|s| s.as_str())
95 }
96
97 pub fn get_hash(&self, key: &str) -> Option<&str> {
99 self.hashes.get(key).map(|hp| hp.hash.as_str())
100 }
101
102 pub fn get_size(&self, key: &str) -> Option<u64> {
104 self.hashes.get(key).map(|hp| hp.size)
105 }
106
107 pub fn get_hash_pair(&self, key: &str) -> Option<&HashPair> {
109 self.hashes.get(key)
110 }
111
112 pub fn has_key(&self, key: &str) -> bool {
114 self.values.contains_key(key)
115 }
116
117 pub fn keys(&self) -> Vec<&str> {
119 self.values.keys().map(|s| s.as_str()).collect()
120 }
121}
122
123pub mod build_keys {
125 pub const ROOT: &str = "root";
127 pub const INSTALL: &str = "install";
129 pub const DOWNLOAD: &str = "download";
131 pub const ENCODING: &str = "encoding";
133 pub const SIZE: &str = "size";
135 pub const PATCH: &str = "patch";
137 pub const PATCH_CONFIG: &str = "patch-config";
139 pub const BUILD_NAME: &str = "build-name";
141 pub const BUILD_UID: &str = "build-uid";
143 pub const BUILD_PRODUCT: &str = "build-product";
145 pub const ENCODING_SIZE: &str = "encoding-size";
147 pub const INSTALL_SIZE: &str = "install-size";
149 pub const DOWNLOAD_SIZE: &str = "download-size";
151 pub const SIZE_SIZE: &str = "size-size";
153 pub const VFS_ROOT: &str = "vfs-root";
155}
156
157pub mod cdn_keys {
159 pub const ARCHIVE_GROUP: &str = "archive-group";
161 pub const ARCHIVES: &str = "archives";
163 pub const PATCH_ARCHIVES: &str = "patch-archives";
165 pub const FILE_INDEX: &str = "file-index";
167 pub const PATCH_FILE_INDEX: &str = "patch-file-index";
169}
170
171fn is_hex_hash(s: &str) -> bool {
173 s.len() >= 6 && s.chars().all(|c| c.is_ascii_hexdigit())
174}
175
176#[derive(Debug, Clone)]
178pub struct BuildConfig {
179 pub config: ConfigFile,
181}
182
183impl BuildConfig {
184 pub fn parse(text: &str) -> Result<Self> {
186 let config = ConfigFile::parse(text)?;
187 Ok(BuildConfig { config })
188 }
189
190 fn extract_hash(&self, key: &str) -> Option<&str> {
192 if let Some(hash) = self.config.get_hash(key) {
194 return Some(hash);
195 }
196
197 if let Some(value) = self.config.get_value(key) {
199 let parts: Vec<&str> = value.split_whitespace().collect();
200 if !parts.is_empty() && is_hex_hash(parts[0]) {
201 return Some(parts[0]);
202 }
203 }
204
205 None
206 }
207
208 pub fn root_hash(&self) -> Option<&str> {
210 self.extract_hash(build_keys::ROOT)
211 }
212
213 pub fn encoding_hash(&self) -> Option<&str> {
215 self.extract_hash(build_keys::ENCODING)
216 }
217
218 pub fn install_hash(&self) -> Option<&str> {
220 self.extract_hash(build_keys::INSTALL)
221 }
222
223 pub fn download_hash(&self) -> Option<&str> {
225 self.extract_hash(build_keys::DOWNLOAD)
226 }
227
228 pub fn size_hash(&self) -> Option<&str> {
230 self.extract_hash(build_keys::SIZE)
231 }
232
233 pub fn build_name(&self) -> Option<&str> {
235 self.config.get_value(build_keys::BUILD_NAME)
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct CdnConfig {
242 pub config: ConfigFile,
244}
245
246impl CdnConfig {
247 pub fn parse(text: &str) -> Result<Self> {
249 let config = ConfigFile::parse(text)?;
250 Ok(CdnConfig { config })
251 }
252
253 pub fn archives(&self) -> Vec<&str> {
255 self.config
256 .get_value(cdn_keys::ARCHIVES)
257 .map(|v| v.split_whitespace().collect())
258 .unwrap_or_default()
259 }
260
261 pub fn archive_group(&self) -> Option<&str> {
263 self.config.get_value(cdn_keys::ARCHIVE_GROUP)
264 }
265
266 pub fn file_index(&self) -> Option<&str> {
268 self.config.get_value(cdn_keys::FILE_INDEX)
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_parse_simple_config() {
278 let config_text = r#"
279# This is a comment
280key1 = value1
281key2 = value2
282
283# Empty lines are ignored
284key3 = value with spaces
285 "#;
286
287 let config = ConfigFile::parse(config_text).unwrap();
288 assert_eq!(config.get_value("key1"), Some("value1"));
289 assert_eq!(config.get_value("key2"), Some("value2"));
290 assert_eq!(config.get_value("key3"), Some("value with spaces"));
291 assert_eq!(config.get_value("nonexistent"), None);
292 }
293
294 #[test]
295 fn test_parse_hash_pairs() {
296 let config_text = r#"
297encoding = abc123def456789 123456
298root = 0123456789abcdef 789
299install = fedcba9876543210 456789
300invalid = not_a_hash 123
301 "#;
302
303 let config = ConfigFile::parse(config_text).unwrap();
304
305 assert_eq!(config.get_hash("encoding"), Some("abc123def456789"));
307 assert_eq!(config.get_size("encoding"), Some(123456));
308
309 assert_eq!(config.get_hash("root"), Some("0123456789abcdef"));
310 assert_eq!(config.get_size("root"), Some(789));
311
312 assert_eq!(config.get_hash("install"), Some("fedcba9876543210"));
313 assert_eq!(config.get_size("install"), Some(456789));
314
315 assert_eq!(config.get_hash("invalid"), None);
317
318 assert_eq!(config.get_value("invalid"), Some("not_a_hash 123"));
320 }
321
322 #[test]
323 fn test_build_config() {
324 let config_text = r#"
325root = abc123 100
326encoding = def456 200
327install = 789abc 300
328download = cdef01 400
329size = 234567 500
330build-name = 10.0.0.12345
331build-uid = wow/game
332 "#;
333
334 let build = BuildConfig::parse(config_text).unwrap();
335
336 assert_eq!(build.root_hash(), Some("abc123"));
337 assert_eq!(build.encoding_hash(), Some("def456"));
338 assert_eq!(build.install_hash(), Some("789abc"));
339 assert_eq!(build.download_hash(), Some("cdef01"));
340 assert_eq!(build.size_hash(), Some("234567"));
341 assert_eq!(build.build_name(), Some("10.0.0.12345"));
342 }
343
344 #[test]
345 fn test_cdn_config() {
346 let config_text = r#"
347archives = archive1 archive2 archive3
348archive-group = abc123def456
349file-index = 789abcdef012
350patch-archives = patch1 patch2
351 "#;
352
353 let cdn = CdnConfig::parse(config_text).unwrap();
354
355 let archives = cdn.archives();
356 assert_eq!(archives.len(), 3);
357 assert_eq!(archives[0], "archive1");
358 assert_eq!(archives[1], "archive2");
359 assert_eq!(archives[2], "archive3");
360
361 assert_eq!(cdn.archive_group(), Some("abc123def456"));
362 assert_eq!(cdn.file_index(), Some("789abcdef012"));
363 }
364
365 #[test]
366 fn test_is_hex_hash() {
367 assert!(is_hex_hash("abc123def456"));
368 assert!(is_hex_hash("0123456789ABCDEF"));
369 assert!(!is_hex_hash("not_hex"));
370 assert!(!is_hex_hash("abc12g")); assert!(!is_hex_hash("abc")); }
373}