1use crate::{Error, Result};
4use ngdp_bpsv::BpsvDocument;
5
6#[derive(Debug, Clone, PartialEq)]
8pub struct VersionEntry {
9 pub region: String,
11 pub build_config: String,
13 pub cdn_config: String,
15 pub key_ring: Option<String>,
17 pub build_id: u32,
19 pub versions_name: String,
21 pub product_config: String,
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct CdnEntry {
28 pub name: String,
30 pub path: String,
32 pub hosts: Vec<String>,
34 pub servers: Vec<String>,
36 pub config_path: String,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct BgdlEntry {
43 pub region: String,
45 pub build_config: String,
47 pub cdn_config: String,
49 pub install_bgdl_config: Option<String>,
51 pub game_bgdl_config: Option<String>,
53}
54
55pub fn parse_versions(content: &str) -> Result<Vec<VersionEntry>> {
57 let doc = BpsvDocument::parse(content)?;
58 let schema = doc.schema();
59
60 let region_idx = schema
62 .get_field("Region")
63 .ok_or_else(|| Error::InvalidManifest {
64 line: 0,
65 reason: "Missing Region field".to_string(),
66 })?
67 .index;
68 let build_config_idx = schema
69 .get_field("BuildConfig")
70 .ok_or_else(|| Error::InvalidManifest {
71 line: 0,
72 reason: "Missing BuildConfig field".to_string(),
73 })?
74 .index;
75 let cdn_config_idx = schema
76 .get_field("CDNConfig")
77 .ok_or_else(|| Error::InvalidManifest {
78 line: 0,
79 reason: "Missing CDNConfig field".to_string(),
80 })?
81 .index;
82 let key_ring_idx = schema.get_field("KeyRing").map(|f| f.index);
83 let build_id_idx = schema
84 .get_field("BuildId")
85 .ok_or_else(|| Error::InvalidManifest {
86 line: 0,
87 reason: "Missing BuildId field".to_string(),
88 })?
89 .index;
90 let versions_name_idx = schema
91 .get_field("VersionsName")
92 .ok_or_else(|| Error::InvalidManifest {
93 line: 0,
94 reason: "Missing VersionsName field".to_string(),
95 })?
96 .index;
97 let product_config_idx = schema
98 .get_field("ProductConfig")
99 .ok_or_else(|| Error::InvalidManifest {
100 line: 0,
101 reason: "Missing ProductConfig field".to_string(),
102 })?
103 .index;
104
105 let mut entries = Vec::with_capacity(doc.rows().len());
106
107 for row in doc.rows() {
108 entries.push(VersionEntry {
109 region: row.get_raw(region_idx).unwrap().to_string(),
110 build_config: row.get_raw(build_config_idx).unwrap().to_string(),
111 cdn_config: row.get_raw(cdn_config_idx).unwrap().to_string(),
112 key_ring: key_ring_idx.and_then(|idx| {
113 row.get_raw(idx).and_then(|s| {
114 if s.is_empty() {
115 None
116 } else {
117 Some(s.to_string())
118 }
119 })
120 }),
121 build_id: row.get_raw(build_id_idx).unwrap().parse().map_err(|_| {
122 Error::InvalidManifest {
123 line: 0,
124 reason: format!("Invalid BuildId: {}", row.get_raw(build_id_idx).unwrap()),
125 }
126 })?,
127 versions_name: row.get_raw(versions_name_idx).unwrap().to_string(),
128 product_config: row.get_raw(product_config_idx).unwrap().to_string(),
129 });
130 }
131
132 Ok(entries)
133}
134
135pub fn parse_cdns(content: &str) -> Result<Vec<CdnEntry>> {
137 let doc = BpsvDocument::parse(content)?;
138 let schema = doc.schema();
139
140 let name_idx = schema
142 .get_field("Name")
143 .ok_or_else(|| Error::InvalidManifest {
144 line: 0,
145 reason: "Missing Name field".to_string(),
146 })?
147 .index;
148 let path_idx = schema
149 .get_field("Path")
150 .ok_or_else(|| Error::InvalidManifest {
151 line: 0,
152 reason: "Missing Path field".to_string(),
153 })?
154 .index;
155 let hosts_idx = schema
156 .get_field("Hosts")
157 .ok_or_else(|| Error::InvalidManifest {
158 line: 0,
159 reason: "Missing Hosts field".to_string(),
160 })?
161 .index;
162 let servers_idx = schema.get_field("Servers").map(|f| f.index);
163 let config_path_idx = schema
164 .get_field("ConfigPath")
165 .ok_or_else(|| Error::InvalidManifest {
166 line: 0,
167 reason: "Missing ConfigPath field".to_string(),
168 })?
169 .index;
170
171 let mut entries = Vec::with_capacity(doc.rows().len());
172
173 for row in doc.rows() {
174 let hosts_str = row.get_raw(hosts_idx).unwrap();
176 let hosts = if hosts_str.is_empty() {
177 Vec::new()
178 } else {
179 hosts_str
180 .split_whitespace()
181 .map(|s| s.to_string())
182 .collect()
183 };
184
185 let servers = servers_idx
187 .and_then(|idx| row.get_raw(idx))
188 .filter(|s| !s.is_empty())
189 .map(|s| {
190 s.split_whitespace()
191 .map(|s| s.to_string())
192 .collect::<Vec<_>>()
193 })
194 .unwrap_or_default();
195
196 entries.push(CdnEntry {
197 name: row.get_raw(name_idx).unwrap().to_string(),
198 path: row.get_raw(path_idx).unwrap().to_string(),
199 hosts,
200 servers,
201 config_path: row.get_raw(config_path_idx).unwrap().to_string(),
202 });
203 }
204
205 Ok(entries)
206}
207
208pub fn parse_bgdl(content: &str) -> Result<Vec<BgdlEntry>> {
210 let doc = BpsvDocument::parse(content)?;
211 let schema = doc.schema();
212
213 let region_idx = schema
215 .get_field("Region")
216 .ok_or_else(|| Error::InvalidManifest {
217 line: 0,
218 reason: "Missing Region field".to_string(),
219 })?
220 .index;
221 let build_config_idx = schema
222 .get_field("BuildConfig")
223 .ok_or_else(|| Error::InvalidManifest {
224 line: 0,
225 reason: "Missing BuildConfig field".to_string(),
226 })?
227 .index;
228 let cdn_config_idx = schema
229 .get_field("CDNConfig")
230 .ok_or_else(|| Error::InvalidManifest {
231 line: 0,
232 reason: "Missing CDNConfig field".to_string(),
233 })?
234 .index;
235 let install_bgdl_idx = schema.get_field("InstallBGDLConfig").map(|f| f.index);
236 let game_bgdl_idx = schema.get_field("GameBGDLConfig").map(|f| f.index);
237
238 let mut entries = Vec::with_capacity(doc.rows().len());
239
240 for row in doc.rows() {
241 entries.push(BgdlEntry {
242 region: row.get_raw(region_idx).unwrap().to_string(),
243 build_config: row.get_raw(build_config_idx).unwrap().to_string(),
244 cdn_config: row.get_raw(cdn_config_idx).unwrap().to_string(),
245 install_bgdl_config: install_bgdl_idx.and_then(|idx| {
246 row.get_raw(idx).and_then(|s| {
247 if s.is_empty() {
248 None
249 } else {
250 Some(s.to_string())
251 }
252 })
253 }),
254 game_bgdl_config: game_bgdl_idx.and_then(|idx| {
255 row.get_raw(idx).and_then(|s| {
256 if s.is_empty() {
257 None
258 } else {
259 Some(s.to_string())
260 }
261 })
262 }),
263 });
264 }
265
266 Ok(entries)
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_parse_cdns_with_servers() {
275 let content = r#"Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0
276us|tpr/wow|blzddist1-a.akamaihd.net level3.blizzard.com|http://blzddist1-a.akamaihd.net/?maxhosts=4 http://level3.blizzard.com/?maxhosts=4|tpr/configs/data
277eu|tpr/wow|blzddist1-a.akamaihd.net level3.blizzard.com|http://eu.cdn.blizzard.com/?maxhosts=4 https://blzddist1-a.akamaihd.net/?fallback=1&maxhosts=4|tpr/configs/data"#;
278
279 let entries = parse_cdns(content).unwrap();
280 assert_eq!(entries.len(), 2);
281
282 let us_cdn = &entries[0];
283 assert_eq!(us_cdn.name, "us");
284 assert_eq!(us_cdn.path, "tpr/wow");
285 assert_eq!(us_cdn.hosts.len(), 2);
286 assert_eq!(us_cdn.hosts[0], "blzddist1-a.akamaihd.net");
287 assert_eq!(us_cdn.hosts[1], "level3.blizzard.com");
288 assert_eq!(us_cdn.servers.len(), 2);
289 assert_eq!(
290 us_cdn.servers[0],
291 "http://blzddist1-a.akamaihd.net/?maxhosts=4"
292 );
293 assert_eq!(us_cdn.servers[1], "http://level3.blizzard.com/?maxhosts=4");
294 }
295
296 #[test]
297 fn test_parse_cdns_without_servers() {
298 let content = r#"Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0
299us|tpr/wow|host1.com host2.com||tpr/configs/data"#;
300
301 let entries = parse_cdns(content).unwrap();
302 assert_eq!(entries.len(), 1);
303
304 let entry = &entries[0];
305 assert_eq!(entry.hosts, vec!["host1.com", "host2.com"]);
306 assert_eq!(entry.servers, Vec::<String>::new());
307 }
308
309 #[test]
310 fn test_parse_versions() {
311 let content = r#"Region!STRING:0|BuildConfig!STRING:0|CDNConfig!STRING:0|KeyRing!STRING:0|BuildId!DEC:4|VersionsName!STRING:0|ProductConfig!STRING:0
312us|abcd1234|efgh5678||12345|1.0.0.12345|ijkl9012
313eu|abcd1234|efgh5678|mnop3456|12345|1.0.0.12345|ijkl9012"#;
314
315 let entries = parse_versions(content).unwrap();
316 assert_eq!(entries.len(), 2);
317
318 let us_version = &entries[0];
319 assert_eq!(us_version.region, "us");
320 assert_eq!(us_version.build_id, 12345);
321 assert!(us_version.key_ring.is_none());
322
323 let eu_version = &entries[1];
324 assert_eq!(eu_version.key_ring, Some("mnop3456".to_string()));
325 }
326
327 #[test]
328 fn test_parse_bgdl() {
329 let content = r#"Region!STRING:0|BuildConfig!STRING:0|CDNConfig!STRING:0|InstallBGDLConfig!STRING:0|GameBGDLConfig!STRING:0
330us|abcd1234|efgh5678|install123|game456
331eu|abcd1234|efgh5678||game789"#;
332
333 let entries = parse_bgdl(content).unwrap();
334 assert_eq!(entries.len(), 2);
335
336 let us_bgdl = &entries[0];
337 assert_eq!(us_bgdl.install_bgdl_config, Some("install123".to_string()));
338 assert_eq!(us_bgdl.game_bgdl_config, Some("game456".to_string()));
339
340 let eu_bgdl = &entries[1];
341 assert!(eu_bgdl.install_bgdl_config.is_none());
342 assert_eq!(eu_bgdl.game_bgdl_config, Some("game789".to_string()));
343 }
344
345 #[test]
346 fn test_ribbit_vs_http_compatibility() {
347 let ribbit_format = r#"Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0
350us|tpr/wow|level3.blizzard.com us.cdn.blizzard.com|http://level3.blizzard.com/?maxhosts=4 http://us.cdn.blizzard.com/?maxhosts=4|tpr/configs/data"#;
351
352 let entries = parse_cdns(ribbit_format).unwrap();
353 let entry = &entries[0];
354
355 assert_eq!(entry.hosts.len(), 2);
357 assert_eq!(entry.servers.len(), 2);
358
359 assert_eq!(entry.hosts[0], "level3.blizzard.com");
361 assert_eq!(entry.hosts[1], "us.cdn.blizzard.com");
362 assert_eq!(entry.servers[0], "http://level3.blizzard.com/?maxhosts=4");
363 assert_eq!(entry.servers[1], "http://us.cdn.blizzard.com/?maxhosts=4");
364 }
365}