use std::collections::HashMap;
use tracing::{debug, trace};
use crate::Result;
#[derive(Debug, Clone, PartialEq)]
pub struct HashPair {
pub hash: String,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct ConfigFile {
pub values: HashMap<String, String>,
pub hashes: HashMap<String, HashPair>,
}
impl ConfigFile {
pub fn parse(text: &str) -> Result<Self> {
let mut values = HashMap::new();
let mut hashes = HashMap::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = if let Some(eq_pos) = line.find(" = ") {
let key = line[..eq_pos].trim();
let value = line[eq_pos + 3..].trim(); (key, value)
} else if let Some(eq_pos) = line.find(" =") {
let key = line[..eq_pos].trim();
let value = line[eq_pos + 2..].trim(); (key, value)
} else {
continue; };
trace!("Config entry: '{}' = '{}'", key, value);
values.insert(key.to_string(), value.to_string());
if !value.is_empty() {
let parts: Vec<&str> = value.split_whitespace().collect();
if !parts.is_empty() && is_hex_hash(parts[0]) {
if parts.len() >= 2 {
if let Ok(size) = parts[1].parse::<u64>() {
hashes.insert(
key.to_string(),
HashPair {
hash: parts[0].to_string(),
size,
},
);
}
}
}
}
}
debug!(
"Parsed config with {} entries, {} hash pairs",
values.len(),
hashes.len()
);
Ok(ConfigFile { values, hashes })
}
pub fn get_value(&self, key: &str) -> Option<&str> {
self.values.get(key).map(|s| s.as_str())
}
pub fn get_hash(&self, key: &str) -> Option<&str> {
self.hashes.get(key).map(|hp| hp.hash.as_str())
}
pub fn get_size(&self, key: &str) -> Option<u64> {
self.hashes.get(key).map(|hp| hp.size)
}
pub fn get_hash_pair(&self, key: &str) -> Option<&HashPair> {
self.hashes.get(key)
}
pub fn has_key(&self, key: &str) -> bool {
self.values.contains_key(key)
}
pub fn keys(&self) -> Vec<&str> {
self.values.keys().map(|s| s.as_str()).collect()
}
}
pub mod build_keys {
pub const ROOT: &str = "root";
pub const INSTALL: &str = "install";
pub const DOWNLOAD: &str = "download";
pub const ENCODING: &str = "encoding";
pub const SIZE: &str = "size";
pub const PATCH: &str = "patch";
pub const PATCH_CONFIG: &str = "patch-config";
pub const BUILD_NAME: &str = "build-name";
pub const BUILD_UID: &str = "build-uid";
pub const BUILD_PRODUCT: &str = "build-product";
pub const ENCODING_SIZE: &str = "encoding-size";
pub const INSTALL_SIZE: &str = "install-size";
pub const DOWNLOAD_SIZE: &str = "download-size";
pub const SIZE_SIZE: &str = "size-size";
pub const VFS_ROOT: &str = "vfs-root";
}
pub mod cdn_keys {
pub const ARCHIVE_GROUP: &str = "archive-group";
pub const ARCHIVES: &str = "archives";
pub const PATCH_ARCHIVES: &str = "patch-archives";
pub const FILE_INDEX: &str = "file-index";
pub const PATCH_FILE_INDEX: &str = "patch-file-index";
}
fn is_hex_hash(s: &str) -> bool {
s.len() >= 6 && s.chars().all(|c| c.is_ascii_hexdigit())
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub config: ConfigFile,
}
impl BuildConfig {
pub fn parse(text: &str) -> Result<Self> {
let config = ConfigFile::parse(text)?;
Ok(BuildConfig { config })
}
fn extract_hash(&self, key: &str) -> Option<&str> {
if let Some(hash) = self.config.get_hash(key) {
return Some(hash);
}
if let Some(value) = self.config.get_value(key) {
let parts: Vec<&str> = value.split_whitespace().collect();
if !parts.is_empty() && is_hex_hash(parts[0]) {
return Some(parts[0]);
}
}
None
}
pub fn root_hash(&self) -> Option<&str> {
self.extract_hash(build_keys::ROOT)
}
pub fn encoding_hash(&self) -> Option<&str> {
self.extract_hash(build_keys::ENCODING)
}
pub fn install_hash(&self) -> Option<&str> {
self.extract_hash(build_keys::INSTALL)
}
pub fn download_hash(&self) -> Option<&str> {
self.extract_hash(build_keys::DOWNLOAD)
}
pub fn size_hash(&self) -> Option<&str> {
self.extract_hash(build_keys::SIZE)
}
pub fn build_name(&self) -> Option<&str> {
self.config.get_value(build_keys::BUILD_NAME)
}
}
#[derive(Debug, Clone)]
pub struct CdnConfig {
pub config: ConfigFile,
}
impl CdnConfig {
pub fn parse(text: &str) -> Result<Self> {
let config = ConfigFile::parse(text)?;
Ok(CdnConfig { config })
}
pub fn archives(&self) -> Vec<&str> {
self.config
.get_value(cdn_keys::ARCHIVES)
.map(|v| v.split_whitespace().collect())
.unwrap_or_default()
}
pub fn archive_group(&self) -> Option<&str> {
self.config.get_value(cdn_keys::ARCHIVE_GROUP)
}
pub fn file_index(&self) -> Option<&str> {
self.config.get_value(cdn_keys::FILE_INDEX)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_config() {
let config_text = r#"
# This is a comment
key1 = value1
key2 = value2
# Empty lines are ignored
key3 = value with spaces
"#;
let config = ConfigFile::parse(config_text).unwrap();
assert_eq!(config.get_value("key1"), Some("value1"));
assert_eq!(config.get_value("key2"), Some("value2"));
assert_eq!(config.get_value("key3"), Some("value with spaces"));
assert_eq!(config.get_value("nonexistent"), None);
}
#[test]
fn test_parse_hash_pairs() {
let config_text = r#"
encoding = abc123def456789 123456
root = 0123456789abcdef 789
install = fedcba9876543210 456789
invalid = not_a_hash 123
"#;
let config = ConfigFile::parse(config_text).unwrap();
assert_eq!(config.get_hash("encoding"), Some("abc123def456789"));
assert_eq!(config.get_size("encoding"), Some(123456));
assert_eq!(config.get_hash("root"), Some("0123456789abcdef"));
assert_eq!(config.get_size("root"), Some(789));
assert_eq!(config.get_hash("install"), Some("fedcba9876543210"));
assert_eq!(config.get_size("install"), Some(456789));
assert_eq!(config.get_hash("invalid"), None);
assert_eq!(config.get_value("invalid"), Some("not_a_hash 123"));
}
#[test]
fn test_build_config() {
let config_text = r#"
root = abc123 100
encoding = def456 200
install = 789abc 300
download = cdef01 400
size = 234567 500
build-name = 10.0.0.12345
build-uid = wow/game
"#;
let build = BuildConfig::parse(config_text).unwrap();
assert_eq!(build.root_hash(), Some("abc123"));
assert_eq!(build.encoding_hash(), Some("def456"));
assert_eq!(build.install_hash(), Some("789abc"));
assert_eq!(build.download_hash(), Some("cdef01"));
assert_eq!(build.size_hash(), Some("234567"));
assert_eq!(build.build_name(), Some("10.0.0.12345"));
}
#[test]
fn test_cdn_config() {
let config_text = r#"
archives = archive1 archive2 archive3
archive-group = abc123def456
file-index = 789abcdef012
patch-archives = patch1 patch2
"#;
let cdn = CdnConfig::parse(config_text).unwrap();
let archives = cdn.archives();
assert_eq!(archives.len(), 3);
assert_eq!(archives[0], "archive1");
assert_eq!(archives[1], "archive2");
assert_eq!(archives[2], "archive3");
assert_eq!(cdn.archive_group(), Some("abc123def456"));
assert_eq!(cdn.file_index(), Some("789abcdef012"));
}
#[test]
fn test_is_hex_hash() {
assert!(is_hex_hash("abc123def456"));
assert!(is_hex_hash("0123456789ABCDEF"));
assert!(!is_hex_hash("not_hex"));
assert!(!is_hex_hash("abc12g")); assert!(!is_hex_hash("abc")); }
}