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
110 .get_raw(region_idx)
111 .ok_or_else(|| Error::missing_field("Region"))?
112 .to_string(),
113 build_config: row
114 .get_raw(build_config_idx)
115 .ok_or_else(|| Error::missing_field("BuildConfig"))?
116 .to_string(),
117 cdn_config: row
118 .get_raw(cdn_config_idx)
119 .ok_or_else(|| Error::missing_field("CDNConfig"))?
120 .to_string(),
121 key_ring: key_ring_idx.and_then(|idx| {
122 row.get_raw(idx).and_then(|s| {
123 if s.is_empty() {
124 None
125 } else {
126 Some(s.to_string())
127 }
128 })
129 }),
130 build_id: row
131 .get_raw(build_id_idx)
132 .ok_or_else(|| Error::missing_field("BuildId"))?
133 .parse()
134 .map_err(|_| {
135 let build_id_str = row.get_raw(build_id_idx).unwrap_or("<missing>"); Error::InvalidManifest {
137 line: 0,
138 reason: format!("Invalid BuildId: {build_id_str}"),
139 }
140 })?,
141 versions_name: row
142 .get_raw(versions_name_idx)
143 .ok_or_else(|| Error::missing_field("VersionsName"))?
144 .to_string(),
145 product_config: row
146 .get_raw(product_config_idx)
147 .ok_or_else(|| Error::missing_field("ProductConfig"))?
148 .to_string(),
149 });
150 }
151
152 Ok(entries)
153}
154
155pub fn parse_cdns(content: &str) -> Result<Vec<CdnEntry>> {
157 let doc = BpsvDocument::parse(content)?;
158 let schema = doc.schema();
159
160 let name_idx = schema
162 .get_field("Name")
163 .ok_or_else(|| Error::InvalidManifest {
164 line: 0,
165 reason: "Missing Name field".to_string(),
166 })?
167 .index;
168 let path_idx = schema
169 .get_field("Path")
170 .ok_or_else(|| Error::InvalidManifest {
171 line: 0,
172 reason: "Missing Path field".to_string(),
173 })?
174 .index;
175 let hosts_idx = schema
176 .get_field("Hosts")
177 .ok_or_else(|| Error::InvalidManifest {
178 line: 0,
179 reason: "Missing Hosts field".to_string(),
180 })?
181 .index;
182 let servers_idx = schema.get_field("Servers").map(|f| f.index);
183 let config_path_idx = schema
184 .get_field("ConfigPath")
185 .ok_or_else(|| Error::InvalidManifest {
186 line: 0,
187 reason: "Missing ConfigPath field".to_string(),
188 })?
189 .index;
190
191 let mut entries = Vec::with_capacity(doc.rows().len());
192
193 for row in doc.rows() {
194 let hosts_str = row
196 .get_raw(hosts_idx)
197 .ok_or_else(|| Error::missing_field("Hosts"))?;
198 let hosts = if hosts_str.is_empty() {
199 Vec::new()
200 } else {
201 hosts_str
202 .split_whitespace()
203 .map(|s| s.to_string())
204 .collect()
205 };
206
207 let servers = servers_idx
209 .and_then(|idx| row.get_raw(idx))
210 .filter(|s| !s.is_empty())
211 .map(|s| {
212 s.split_whitespace()
213 .map(|s| s.to_string())
214 .collect::<Vec<_>>()
215 })
216 .unwrap_or_default();
217
218 entries.push(CdnEntry {
219 name: row
220 .get_raw(name_idx)
221 .ok_or_else(|| Error::missing_field("Name"))?
222 .to_string(),
223 path: row
224 .get_raw(path_idx)
225 .ok_or_else(|| Error::missing_field("Path"))?
226 .to_string(),
227 hosts,
228 servers,
229 config_path: row
230 .get_raw(config_path_idx)
231 .ok_or_else(|| Error::missing_field("ConfigPath"))?
232 .to_string(),
233 });
234 }
235
236 Ok(entries)
237}
238
239pub fn parse_bgdl(content: &str) -> Result<Vec<BgdlEntry>> {
241 let doc = BpsvDocument::parse(content)?;
242 let schema = doc.schema();
243
244 let region_idx = schema
246 .get_field("Region")
247 .ok_or_else(|| Error::InvalidManifest {
248 line: 0,
249 reason: "Missing Region field".to_string(),
250 })?
251 .index;
252 let build_config_idx = schema
253 .get_field("BuildConfig")
254 .ok_or_else(|| Error::InvalidManifest {
255 line: 0,
256 reason: "Missing BuildConfig field".to_string(),
257 })?
258 .index;
259 let cdn_config_idx = schema
260 .get_field("CDNConfig")
261 .ok_or_else(|| Error::InvalidManifest {
262 line: 0,
263 reason: "Missing CDNConfig field".to_string(),
264 })?
265 .index;
266 let install_bgdl_idx = schema.get_field("InstallBGDLConfig").map(|f| f.index);
267 let game_bgdl_idx = schema.get_field("GameBGDLConfig").map(|f| f.index);
268
269 let mut entries = Vec::with_capacity(doc.rows().len());
270
271 for row in doc.rows() {
272 entries.push(BgdlEntry {
273 region: row
274 .get_raw(region_idx)
275 .ok_or_else(|| Error::missing_field("Region"))?
276 .to_string(),
277 build_config: row
278 .get_raw(build_config_idx)
279 .ok_or_else(|| Error::missing_field("BuildConfig"))?
280 .to_string(),
281 cdn_config: row
282 .get_raw(cdn_config_idx)
283 .ok_or_else(|| Error::missing_field("CDNConfig"))?
284 .to_string(),
285 install_bgdl_config: install_bgdl_idx.and_then(|idx| {
286 row.get_raw(idx).and_then(|s| {
287 if s.is_empty() {
288 None
289 } else {
290 Some(s.to_string())
291 }
292 })
293 }),
294 game_bgdl_config: game_bgdl_idx.and_then(|idx| {
295 row.get_raw(idx).and_then(|s| {
296 if s.is_empty() {
297 None
298 } else {
299 Some(s.to_string())
300 }
301 })
302 }),
303 });
304 }
305
306 Ok(entries)
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_parse_cdns_with_servers() {
315 let content = r#"Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0
316us|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
317eu|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"#;
318
319 let entries = parse_cdns(content).unwrap();
320 assert_eq!(entries.len(), 2);
321
322 let us_cdn = &entries[0];
323 assert_eq!(us_cdn.name, "us");
324 assert_eq!(us_cdn.path, "tpr/wow");
325 assert_eq!(us_cdn.hosts.len(), 2);
326 assert_eq!(us_cdn.hosts[0], "blzddist1-a.akamaihd.net");
327 assert_eq!(us_cdn.hosts[1], "level3.blizzard.com");
328 assert_eq!(us_cdn.servers.len(), 2);
329 assert_eq!(
330 us_cdn.servers[0],
331 "http://blzddist1-a.akamaihd.net/?maxhosts=4"
332 );
333 assert_eq!(us_cdn.servers[1], "http://level3.blizzard.com/?maxhosts=4");
334 }
335
336 #[test]
337 fn test_parse_cdns_without_servers() {
338 let content = r#"Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0
339us|tpr/wow|host1.com host2.com||tpr/configs/data"#;
340
341 let entries = parse_cdns(content).unwrap();
342 assert_eq!(entries.len(), 1);
343
344 let entry = &entries[0];
345 assert_eq!(entry.hosts, vec!["host1.com", "host2.com"]);
346 assert_eq!(entry.servers, Vec::<String>::new());
347 }
348
349 #[test]
350 fn test_parse_versions() {
351 let content = r#"Region!STRING:0|BuildConfig!STRING:0|CDNConfig!STRING:0|KeyRing!STRING:0|BuildId!DEC:4|VersionsName!STRING:0|ProductConfig!STRING:0
352us|abcd1234|efgh5678||12345|1.0.0.12345|ijkl9012
353eu|abcd1234|efgh5678|mnop3456|12345|1.0.0.12345|ijkl9012"#;
354
355 let entries = parse_versions(content).unwrap();
356 assert_eq!(entries.len(), 2);
357
358 let us_version = &entries[0];
359 assert_eq!(us_version.region, "us");
360 assert_eq!(us_version.build_id, 12345);
361 assert!(us_version.key_ring.is_none());
362
363 let eu_version = &entries[1];
364 assert_eq!(eu_version.key_ring, Some("mnop3456".to_string()));
365 }
366
367 #[test]
368 fn test_parse_bgdl() {
369 let content = r#"Region!STRING:0|BuildConfig!STRING:0|CDNConfig!STRING:0|InstallBGDLConfig!STRING:0|GameBGDLConfig!STRING:0
370us|abcd1234|efgh5678|install123|game456
371eu|abcd1234|efgh5678||game789"#;
372
373 let entries = parse_bgdl(content).unwrap();
374 assert_eq!(entries.len(), 2);
375
376 let us_bgdl = &entries[0];
377 assert_eq!(us_bgdl.install_bgdl_config, Some("install123".to_string()));
378 assert_eq!(us_bgdl.game_bgdl_config, Some("game456".to_string()));
379
380 let eu_bgdl = &entries[1];
381 assert!(eu_bgdl.install_bgdl_config.is_none());
382 assert_eq!(eu_bgdl.game_bgdl_config, Some("game789".to_string()));
383 }
384
385 #[test]
386 fn test_ribbit_vs_http_compatibility() {
387 let ribbit_format = r#"Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0
390us|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"#;
391
392 let entries = parse_cdns(ribbit_format).unwrap();
393 let entry = &entries[0];
394
395 assert_eq!(entry.hosts.len(), 2);
397 assert_eq!(entry.servers.len(), 2);
398
399 assert_eq!(entry.hosts[0], "level3.blizzard.com");
401 assert_eq!(entry.hosts[1], "us.cdn.blizzard.com");
402 assert_eq!(entry.servers[0], "http://level3.blizzard.com/?maxhosts=4");
403 assert_eq!(entry.servers[1], "http://us.cdn.blizzard.com/?maxhosts=4");
404 }
405}