Skip to main content

casc_lib/config/
build_config.rs

1//! Parser for CASC build configuration files.
2//!
3//! The build config is a `key = value` text file (with `#` comment lines) identified
4//! by the `build_key` hash from `.build.info`.
5//! It contains the root CKey, encoding CKey/EKey pair, install and download keys,
6//! and build metadata.
7
8use std::collections::HashMap;
9
10use crate::error::Result;
11
12/// Parsed build configuration from a CASC build config file.
13///
14/// Fields like `encoding` and `install` contain two space-separated hashes
15/// (content key, then encoding key). Sizes are space-separated integers.
16#[derive(Debug, Clone)]
17pub struct BuildConfig {
18    /// Content key (CKey) of the root file.
19    pub root_ckey: String,
20    /// Content key (CKey) of the encoding file.
21    pub encoding_ckey: String,
22    /// Encoding key (EKey) of the encoding file.
23    pub encoding_ekey: String,
24    /// Encoding file sizes (content size and encoded size).
25    pub encoding_size: Vec<u64>,
26    /// Content key (CKey) of the install file.
27    pub install_ckey: String,
28    /// Encoding key (EKey) of the install file.
29    pub install_ekey: String,
30    /// Content key (CKey) of the download file.
31    pub download_ckey: String,
32    /// Encoding key (EKey) of the download file.
33    pub download_ekey: String,
34    /// Human-readable build name (e.g. `"WOW-66192patch12.0.1_Retail"`).
35    pub build_name: String,
36    /// Build UID used for product identification (e.g. `"wow"`).
37    pub build_uid: String,
38    /// Build product name (e.g. `"WoW"`).
39    pub build_product: String,
40    /// Raw key-value store for all fields (including vfs-*, patch-*, etc.)
41    pub raw: HashMap<String, String>,
42}
43
44/// Parse a CASC build config (key = value format, `#` comments).
45pub fn parse_build_config(content: &str) -> Result<BuildConfig> {
46    let mut raw: HashMap<String, String> = HashMap::new();
47
48    for line in content.lines() {
49        let trimmed = line.trim();
50        if trimmed.is_empty() || trimmed.starts_with('#') {
51            continue;
52        }
53
54        if let Some((key, value)) = trimmed.split_once(" = ") {
55            raw.insert(key.to_string(), value.to_string());
56        }
57    }
58
59    let get = |key: &str| -> String { raw.get(key).cloned().unwrap_or_default() };
60
61    let split_pair = |key: &str| -> (String, String) {
62        let val = get(key);
63        let mut parts = val.splitn(2, ' ');
64        let first = parts.next().unwrap_or("").to_string();
65        let second = parts.next().unwrap_or("").to_string();
66        (first, second)
67    };
68
69    let parse_sizes = |key: &str| -> Vec<u64> {
70        let val = get(key);
71        if val.is_empty() {
72            return Vec::new();
73        }
74        val.split(' ')
75            .filter_map(|s| s.parse::<u64>().ok())
76            .collect()
77    };
78
79    let (encoding_ckey, encoding_ekey) = split_pair("encoding");
80    let (install_ckey, install_ekey) = split_pair("install");
81    let (download_ckey, download_ekey) = split_pair("download");
82
83    Ok(BuildConfig {
84        root_ckey: get("root"),
85        encoding_ckey,
86        encoding_ekey,
87        encoding_size: parse_sizes("encoding-size"),
88        install_ckey,
89        install_ekey,
90        download_ckey,
91        download_ekey,
92        build_name: get("build-name"),
93        build_uid: get("build-uid"),
94        build_product: get("build-product"),
95        raw,
96    })
97}
98
99/// Convert a config hash to a CDN-style file path.
100///
101/// `"13e1eb56..."` becomes `"config/13/e1/13e1eb56..."`
102pub fn config_path(hash: &str) -> String {
103    format!("config/{}/{}/{}", &hash[..2], &hash[2..4], hash)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    const FIXTURE: &str = "# Build Configuration\nroot = 0ff1247849a5cd6049624d3a105811f8\ninstall = a33a459aa7585d626a3e4209858b4eed a9f3ece675323e1bbd2c17a765adc3c4\ninstall-size = 23286 22368\ndownload = 108fa2da5f5337d8eb4e35e0d3573925 697b5503715187be840f15ee5862adf4\ndownload-size = 72504201 61674341\nencoding = d2f601fe389b9a1133709e716899a633 d3e25753d9f33b6ab55c56532e2131cc\nencoding-size = 194221474 182793061\nbuild-name = WOW-66192patch12.0.1_Retail\nbuild-uid = wow\nbuild-product = WoW\n";
111
112    #[test]
113    fn parse_root_ckey() {
114        let config = parse_build_config(FIXTURE).unwrap();
115        assert_eq!(config.root_ckey, "0ff1247849a5cd6049624d3a105811f8");
116    }
117
118    #[test]
119    fn parse_encoding_pair() {
120        let config = parse_build_config(FIXTURE).unwrap();
121        assert_eq!(config.encoding_ckey, "d2f601fe389b9a1133709e716899a633");
122        assert_eq!(config.encoding_ekey, "d3e25753d9f33b6ab55c56532e2131cc");
123    }
124
125    #[test]
126    fn parse_encoding_sizes() {
127        let config = parse_build_config(FIXTURE).unwrap();
128        assert_eq!(config.encoding_size, vec![194221474, 182793061]);
129    }
130
131    #[test]
132    fn parse_build_name() {
133        let config = parse_build_config(FIXTURE).unwrap();
134        assert_eq!(config.build_name, "WOW-66192patch12.0.1_Retail");
135    }
136
137    #[test]
138    fn parse_build_uid() {
139        let config = parse_build_config(FIXTURE).unwrap();
140        assert_eq!(config.build_uid, "wow");
141    }
142
143    #[test]
144    fn parse_skips_comments() {
145        let data = "# comment\nroot = abc123\n";
146        let config = parse_build_config(data).unwrap();
147        assert_eq!(config.root_ckey, "abc123");
148    }
149
150    #[test]
151    fn parse_skips_empty_lines() {
152        let data = "\n\nroot = abc123\n\n";
153        let config = parse_build_config(data).unwrap();
154        assert_eq!(config.root_ckey, "abc123");
155    }
156
157    #[test]
158    fn config_path_from_hash() {
159        let path = config_path("13e1eb56839dfaf734d7fab21b0c8ea4");
160        assert_eq!(path, "config/13/e1/13e1eb56839dfaf734d7fab21b0c8ea4");
161    }
162}