casc_lib/config/
build_info.rs1use std::collections::HashMap;
8
9use crate::error::{CascError, Result};
10
11#[derive(Debug, Clone)]
17pub struct BuildInfo {
18 pub branch: String,
20 pub active: bool,
22 pub build_key: String,
24 pub cdn_key: String,
26 pub cdn_path: String,
28 pub cdn_hosts: Vec<String>,
30 pub version: String,
32 pub product: String,
34 pub tags: String,
36 pub keyring: String,
38}
39
40pub fn list_products(entries: &[BuildInfo]) -> Vec<(&str, &str)> {
42 entries
43 .iter()
44 .map(|e| (e.product.as_str(), e.version.as_str()))
45 .collect()
46}
47
48pub fn parse_build_info(content: &str) -> Result<Vec<BuildInfo>> {
53 let mut lines = content.lines();
54
55 let header = lines
56 .next()
57 .ok_or_else(|| CascError::InvalidFormat("empty .build.info".into()))?;
58
59 let columns: Vec<&str> = header
61 .split('|')
62 .map(|col| col.split('!').next().unwrap_or(col))
63 .collect();
64
65 let index: HashMap<&str, usize> = columns
67 .iter()
68 .enumerate()
69 .map(|(i, &name)| (name, i))
70 .collect();
71
72 let get = |row: &[&str], key: &str| -> String {
73 index
74 .get(key)
75 .and_then(|&i| row.get(i))
76 .map(|s| s.to_string())
77 .unwrap_or_default()
78 };
79
80 let mut entries = Vec::new();
81
82 for line in lines {
83 if line.trim().is_empty() {
84 continue;
85 }
86
87 let fields: Vec<&str> = line.split('|').collect();
88
89 let active_str = get(&fields, "Active");
90 let active = active_str == "1";
91
92 let cdn_hosts_raw = get(&fields, "CDN Hosts");
93 let cdn_hosts: Vec<String> = if cdn_hosts_raw.is_empty() {
94 Vec::new()
95 } else {
96 cdn_hosts_raw.split(' ').map(String::from).collect()
97 };
98
99 entries.push(BuildInfo {
100 branch: get(&fields, "Branch"),
101 active,
102 build_key: get(&fields, "Build Key"),
103 cdn_key: get(&fields, "CDN Key"),
104 cdn_path: get(&fields, "CDN Path"),
105 cdn_hosts,
106 version: get(&fields, "Version"),
107 product: get(&fields, "Product"),
108 tags: get(&fields, "Tags"),
109 keyring: get(&fields, "KeyRing"),
110 });
111 }
112
113 Ok(entries)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 const FIXTURE: &str = "Branch!STRING:0|Active!DEC:1|Build Key!HEX:16|CDN Key!HEX:16|Install Key!HEX:16|IM Size!DEC:4|CDN Path!STRING:0|CDN Hosts!STRING:0|CDN Servers!STRING:0|Tags!STRING:0|Armadillo!STRING:0|Last Activated!STRING:0|Version!STRING:0|KeyRing!HEX:16|Product!STRING:0\neu|1|13e1eb56839dfaf734d7fab21b0c8ea4|36b8057b5cb2175a551325240251f1c0|||tpr/wow|level3.blizzard.com blzddist1-a.akamaihd.net eu.cdn.blizzard.com|blizzard cdn|Windows?enUS?EU?speechoptions=enUS,enGB,deDE,esES,esMX,frFR,itIT,ptBR,ruRU,koKR,zhTW,zhCN|316c4a8ec31d3948a0e3ad5bd6be86f8||12.0.1.66192|3ca57fe7319a297346440e4d2a03a0cd|wow\neu|1|df2221c87fa81a64523f02a0b31d9586|36b8057b5cb2175a551325240251f1c0|||tpr/wow|level3.blizzard.com blzddist1-a.akamaihd.net eu.cdn.blizzard.com|blizzard cdn|Windows?enUS?EU?speechoptions=enUS,enGB,deDE,esES,esMX,frFR,itIT,koKR,ptBR,ruRU,zhCN,zhTW|||||wow_anniversary";
121
122 #[test]
123 fn parse_two_products() {
124 let infos = parse_build_info(FIXTURE).unwrap();
125 assert_eq!(infos.len(), 2);
126 }
127
128 #[test]
129 fn parse_retail_product() {
130 let infos = parse_build_info(FIXTURE).unwrap();
131 let retail = infos.iter().find(|i| i.product == "wow").unwrap();
132 assert_eq!(retail.build_key, "13e1eb56839dfaf734d7fab21b0c8ea4");
133 assert_eq!(retail.cdn_key, "36b8057b5cb2175a551325240251f1c0");
134 assert_eq!(retail.version, "12.0.1.66192");
135 assert!(retail.active);
136 }
137
138 #[test]
139 fn parse_anniversary_product() {
140 let infos = parse_build_info(FIXTURE).unwrap();
141 let anni = infos
142 .iter()
143 .find(|i| i.product == "wow_anniversary")
144 .unwrap();
145 assert_eq!(anni.build_key, "df2221c87fa81a64523f02a0b31d9586");
146 assert_eq!(anni.version, ""); assert!(anni.active);
148 }
149
150 #[test]
151 fn parse_inactive_filtered() {
152 let data =
153 "Branch!STRING:0|Active!DEC:1|Build Key!HEX:16|Product!STRING:0\neu|0|abc|test_product";
154 let infos = parse_build_info(data).unwrap();
155 assert!(!infos.iter().all(|i| i.active)); let inactive = infos.iter().find(|i| i.product == "test_product").unwrap();
157 assert!(!inactive.active);
158 }
159
160 #[test]
161 fn parse_empty_returns_empty() {
162 let data = "Branch!STRING:0|Active!DEC:1|Build Key!HEX:16|Product!STRING:0\n";
163 let infos = parse_build_info(data).unwrap();
164 assert!(infos.is_empty());
165 }
166
167 #[test]
168 fn list_products_returns_all() {
169 let infos = parse_build_info(FIXTURE).unwrap();
170 let products = list_products(&infos);
171 assert_eq!(products.len(), 2);
172 assert_eq!(products[0].0, "wow");
173 assert_eq!(products[0].1, "12.0.1.66192");
174 assert_eq!(products[1].0, "wow_anniversary");
175 assert_eq!(products[1].1, "");
176 }
177
178 #[test]
179 fn list_products_empty_entries() {
180 let products = list_products(&[]);
181 assert!(products.is_empty());
182 }
183}