1extern crate custom_error;
2use crate::ParserError::*;
3use custom_error::custom_error;
4use std::{fs::File, io::Read};
5
6#[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd)]
8pub struct Mod {
9 pub id: i64,
10 pub name: String,
11 pub from_steam: bool,
12 pub link: String,
13}
14
15#[derive(Clone, PartialEq, Debug)]
17pub struct Preset {
18 pub name: String,
19 pub mods: Vec<Mod>,
20}
21
22custom_error! {pub TagErrType
23 HtmlRoot = "Unable to find root html tag. Is this even a html file?",
24 PresetName = "Unable to find PresetName metatag",
25 Modlist = "Unable to find mod-list div",
26 ModTable = "Unable to find table tag",
27 DataTypeText = "Unable to find text in data-type attribute children",
28 ModLinkAnchor = "Unable to find remote mod link",
29 ModLinkSpan = "Unable to find local mod link",
30}
31
32custom_error! {pub ParserError
33 StringConvFail = "Failed to read file to string",
34 DocParseErr = "Failed to parse document with error",
35 DocReadErr = "Failed to read file from path",
36 TagFindErr{tag_type: TagErrType} = "Unable to find tag with error: {tag_type}"
37}
38
39impl Preset {
40 pub fn from_file(file: File) -> Result<Self, ParserError> {
56 parse(file)
57 }
58
59 pub fn from_fs(path: String) -> Result<Self, ParserError> {
70 let file: File = match File::open(path) {
71 Ok(f) => f,
72 Err(_) => return Err(DocReadErr),
73 };
74 parse(file)
75 }
76
77 pub fn as_mods(&self) -> Vec<Mod> {
88 let mut vec = vec![];
89 for _mod in &self.mods {
90 vec.push(_mod.clone())
91 }
92 vec
93 }
94
95 pub fn as_ids(&self) -> Vec<i64> {
106 let mut vec = vec![];
107 for _mod in &self.mods {
108 vec.push(_mod.clone().id)
109 }
110 vec
111 }
112
113 pub fn as_names(&self) -> Vec<String> {
124 let mut vec = vec![];
125 for _mod in &self.mods {
126 vec.push(_mod.clone().name)
127 }
128 vec
129 }
130
131 pub fn as_links(&self) -> Vec<String> {
142 let mut vec = vec![];
143 for _mod in &self.mods {
144 vec.push(_mod.clone().link)
145 }
146 vec
147 }
148}
149
150fn parse(mut file: File) -> Result<Preset, ParserError> {
154 let mut contents: String = "".to_string();
155
156 match file.read_to_string(&mut contents) {
157 Ok(_) => {}
158 Err(_) => return Err(StringConvFail),
159 };
160
161 match roxmltree::Document::parse(&contents) {
162 Ok(doc) => {
163 let mut preset: Preset = Preset {
164 name: "".to_string(),
165 mods: vec![],
166 };
167 let html_node = match doc.root().children().find(|n| n.has_tag_name("html")) {
168 Some(n) => n,
169 None => return Err(TagFindErr{ tag_type: TagErrType::HtmlRoot }),
170 };
171 for node in html_node.children().filter(|n| n.is_element()) {
172 if node.has_tag_name("head") {
173 let tag = match node.children().find(|n| {
174 n.has_tag_name("meta") && n.attribute("name") == Some("arma:PresetName")
175 }) {
176 Some(n) => n,
177 None => return Err(TagFindErr{ tag_type: TagErrType::PresetName}),
178 };
179 preset.name = tag.attribute("content").unwrap().parse().unwrap();
180 } else if node.has_tag_name("body") {
181 let im1 = match node
182 .children()
183 .find(|n| n.has_tag_name("div") && n.attribute("class") == Some("mod-list"))
184 {
185 Some(n) => n,
186 None => return Err(TagFindErr{ tag_type: TagErrType::Modlist }),
187 };
188 let im2 = match im1.children().find(|n| n.has_tag_name("table")) {
189 Some(n) => n,
190 None => return Err(TagFindErr{ tag_type: TagErrType::ModTable }),
191 };
192 let im3 = im2.children().filter(|n| {
193 n.has_tag_name("tr") && n.attribute("data-type") == Some("ModContainer")
194 });
195 for mod_cont in im3 {
196 let mut temp_mod = Mod {
197 id: 0,
198 name: "".to_string(),
199 from_steam: false,
200 link: "".to_string(),
201 };
202 for item in mod_cont.children().filter(|n| n.is_element()) {
203 if item.has_attribute("data-type") {
204 temp_mod.name = match item.children().find(|n| n.is_text()) {
205 Some(n) => n,
206 None => return Err(TagFindErr{ tag_type: TagErrType::DataTypeText }),
207 }
208 .text()
209 .unwrap()
210 .parse()
211 .unwrap();
212 } else {
213 if item
214 .children()
215 .find(|n| n.attribute("class") == Some("from-steam"))
216 .is_some()
217 {
218 temp_mod.from_steam = true;
219 } else if item
220 .children()
221 .find(|n| n.attribute("class") == Some("from-local"))
222 .is_some()
223 {
224 temp_mod.from_steam = false;
225 } else {
226 if item.children().find(|n| n.has_tag_name("a")).is_some() {
227 temp_mod.link =
228 match item.children().find(|n| n.has_tag_name("a")) {
229 Some(n) => n,
230 None => return Err(TagFindErr{ tag_type: TagErrType::ModLinkAnchor }),
231 }
232 .attribute("href")
233 .unwrap()
234 .parse()
235 .unwrap();
236 temp_mod.id = temp_mod.link.replace("http://steamcommunity.com/sharedfiles/filedetails/?id=", "").parse().unwrap()
237 } else {
238 temp_mod.link = match item
239 .children()
240 .find(|n| n.has_tag_name("span"))
241 {
242 Some(n) => n,
243 None => return Err(TagFindErr{ tag_type: TagErrType::ModLinkSpan }),
244 }
245 .attribute("data-meta")
246 .unwrap()
247 .parse()
248 .unwrap();
249 }
250 }
251 }
252 }
253 preset.mods.push(temp_mod);
254 }
255 }
256 }
257 Ok(preset)
258 }
259 Err(_) => return Err(DocParseErr),
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use crate::{parse, Mod, Preset};
266 use std::fs::File;
267
268 #[test]
269 fn parse_file() {
270 let preset = Preset {
271 name: "Parser Test".parse().unwrap(),
272 mods: vec![
273 Mod {
274 id: 0,
275 name: "Ryan\'s ACE Canteen".parse().unwrap(),
276 from_steam: false,
277 link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
278 .parse()
279 .unwrap(),
280 },
281 Mod {
282 id: 450814997,
283 name: "CBA_A3".parse().unwrap(),
284 from_steam: true,
285 link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
286 .parse()
287 .unwrap(),
288 },
289 Mod {
290 id: 463939057,
291 name: "ace".parse().unwrap(),
292 from_steam: true,
293 link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
294 .parse()
295 .unwrap(),
296 },
297 ],
298 };
299 assert_eq!(
300 parse(File::open("tests/samples/Arma 3 Preset Parser Test.html").unwrap()).unwrap(),
301 preset
302 );
303 }
304
305 #[test]
306 fn parse_from_fs() {
307 let preset: Preset = Preset {
308 name: "Parser Test".parse().unwrap(),
309 mods: vec![
310 Mod {
311 id: 0,
312 name: "Ryan\'s ACE Canteen".parse().unwrap(),
313 from_steam: false,
314 link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
315 .parse()
316 .unwrap(),
317 },
318 Mod {
319 id: 450814997,
320 name: "CBA_A3".parse().unwrap(),
321 from_steam: true,
322 link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
323 .parse()
324 .unwrap(),
325 },
326 Mod {
327 id: 463939057,
328 name: "ace".parse().unwrap(),
329 from_steam: true,
330 link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
331 .parse()
332 .unwrap(),
333 },
334 ],
335 };
336 assert_eq!(
337 Preset::from_fs(
338 "tests/samples/Arma 3 Preset Parser Test.html"
339 .parse()
340 .unwrap()
341 )
342 .unwrap(),
343 preset
344 );
345 }
346 #[test]
347 fn parse_from_file() {
348 let preset: Preset = Preset {
349 name: "Parser Test".parse().unwrap(),
350 mods: vec![
351 Mod {
352 id: 0,
353 name: "Ryan\'s ACE Canteen".parse().unwrap(),
354 from_steam: false,
355 link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
356 .parse()
357 .unwrap(),
358 },
359 Mod {
360 id: 450814997,
361 name: "CBA_A3".parse().unwrap(),
362 from_steam: true,
363 link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
364 .parse()
365 .unwrap(),
366 },
367 Mod {
368 id: 463939057,
369 name: "ace".parse().unwrap(),
370 from_steam: true,
371 link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
372 .parse()
373 .unwrap(),
374 },
375 ],
376 };
377 assert_eq!(
378 Preset::from_file(
379 File::open("tests/samples/Arma 3 Preset Parser Test.html").unwrap()
380 )
381 .unwrap(),
382 preset
383 );
384 }
385}