hosts_parser/
lib.rs

1#[macro_use]
2extern crate lazy_static;
3extern crate regex;
4
5use regex::Regex;
6use std::fmt;
7use std::str::FromStr;
8use std::vec::Vec;
9
10#[derive(Debug, Eq, PartialEq)]
11pub struct HostsFile {
12    pub lines: Vec<HostsFileLine>,
13}
14
15#[derive(Debug, Eq, PartialEq)]
16pub struct HostsFileLine {
17    is_empty: bool,
18    comment: Option<String>,
19    ip: Option<String>,
20    hosts: Option<Vec<String>>,
21}
22
23impl fmt::Display for HostsFileLine {
24    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25        // let out = match self {
26        //     HostsFileLine::Empty => "".to_string(),
27        //     HostsFileLine::Comment(s) => format!("#{}", s),
28        //     HostsFileLine::Host(h) => format!("{} {}", h.ip, h.hosts.join(" ")),
29        //     // write!(f, "Error parsing hosts file")
30        // };
31        let mut parts: Vec<Option<String>> = vec![self.ip.clone()];
32        if let Some(hosts) = self.hosts.clone() {
33            let mut clone: Vec<Option<String>> =
34                hosts.clone().iter_mut().map(|h| Some(h.clone())).collect();
35            parts.append(&mut clone);
36        }
37        parts.push(self.comment.clone());
38        let parts: Vec<String> = parts
39            .iter()
40            .filter(|s| s.is_some())
41            .map(|s| s.clone().unwrap())
42            .collect();
43        write!(f, "{}", parts.join(" "))
44    }
45}
46
47impl FromStr for HostsFileLine {
48    type Err = ParseError;
49    fn from_str(s: &str) -> Result<HostsFileLine, Self::Err> {
50        HostsFileLine::from_string(s)
51    }
52}
53
54impl HostsFileLine {
55    pub fn from_empty() -> HostsFileLine {
56        HostsFileLine {
57            is_empty: true,
58            comment: None,
59            ip: None,
60            hosts: None,
61        }
62    }
63    pub fn from_comment(c: &str) -> HostsFileLine {
64        HostsFileLine {
65            is_empty: false,
66            comment: Some(c.to_string()),
67            ip: None,
68            hosts: None,
69        }
70    }
71    pub fn from_string(line: &str) -> Result<HostsFileLine, ParseError> {
72        let line = line.trim();
73        if line == "" {
74            return Ok(HostsFileLine::from_empty());
75        }
76        lazy_static! {
77            static ref COMMENT_RE: Regex = Regex::new(r"^#.*").unwrap();
78        }
79        if COMMENT_RE.is_match(line) {
80            return Ok(HostsFileLine::from_comment(line));
81        }
82        let slices: Vec<String> = line.split_whitespace().map(|s| s.to_string()).collect();
83        let ip: String = slices.first().ok_or(ParseError)?.clone();
84        let hosts: Vec<String> = (&slices[1..])
85            .iter()
86            .take_while(|s| !COMMENT_RE.is_match(s))
87            .map(|h| h.to_string())
88            .collect();
89        if hosts.is_empty() {
90            return Err(ParseError);
91        }
92        let comment: String = (&slices[1..])
93            .iter()
94            .skip_while(|s| !COMMENT_RE.is_match(s))
95            .map(|h| h.to_string())
96            .collect::<Vec<String>>()
97            .join(" ");
98        let comment = match comment.as_str() {
99            "" => None,
100            _ => Some(comment.to_string()),
101        };
102        Ok(HostsFileLine {
103            is_empty: false,
104            ip: Some(ip),
105            hosts: Some(hosts),
106            comment,
107        })
108    }
109    pub fn ip(&self) -> Option<String> {
110        self.ip.clone()
111    }
112    pub fn hosts(&self) -> Vec<String> {
113        self.hosts.clone().unwrap_or_else(|| vec![])
114    }
115    pub fn comment(&self) -> Option<String> {
116        self.comment.clone()
117    }
118    pub fn has_host(&self) -> bool {
119        self.ip.is_some()
120    }
121    pub fn has_comment(&self) -> bool {
122        self.comment.is_some()
123    }
124}
125
126#[derive(Debug, Eq, PartialEq)]
127pub struct HostsFileHost {
128    pub ip: String,
129    pub hosts: Vec<String>,
130    pub comment: Option<String>,
131}
132
133pub struct ParseError;
134
135impl fmt::Display for ParseError {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        write!(f, "Error parsing hosts file")
138    }
139}
140
141impl fmt::Debug for ParseError {
142    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
143        write!(f, "{{ file: {}, line: {} }}", file!(), line!())
144    }
145}
146
147impl FromStr for HostsFile {
148    type Err = ParseError;
149    fn from_str(s: &str) -> Result<HostsFile, Self::Err> {
150        HostsFile::from_string(s)
151    }
152}
153impl HostsFile {
154    fn from_string(s: &str) -> Result<HostsFile, ParseError> {
155        let lines: Vec<HostsFileLine> = s
156            .lines()
157            .map(|l| l.parse::<HostsFileLine>())
158            .collect::<Result<Vec<HostsFileLine>, ParseError>>()?;
159        Ok(HostsFile { lines })
160    }
161    pub fn serialize(&self) -> String {
162        format!(
163            "{}\n",
164            self.lines
165                .iter()
166                .map(|l| format!("{}", l))
167                .collect::<Vec<String>>()
168                .join("\n")
169        )
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    // Parse tests
178    #[test]
179    fn from_empty() {
180        let parsed = HostsFileLine::from_empty();
181        let expected = HostsFileLine {
182            is_empty: true,
183            ip: None,
184            comment: None,
185            hosts: None,
186        };
187        assert_eq!(parsed, expected);
188    }
189    #[test]
190    fn from_comment() {
191        let parsed = HostsFileLine::from_comment("#test");
192        let expected = HostsFileLine {
193            is_empty: false,
194            ip: None,
195            comment: Some("#test".to_string()),
196            hosts: None,
197        };
198        assert_eq!(parsed, expected);
199    }
200
201    #[test]
202    fn empty_line_from_string() {
203        let parsed = HostsFileLine::from_string("").unwrap();
204        let expected = HostsFileLine::from_empty();
205        assert_eq!(parsed, expected);
206    }
207    #[test]
208    fn comment_from_string() {
209        let parsed = HostsFileLine::from_string("# comment").unwrap();
210        let expected = HostsFileLine::from_comment("# comment");
211        assert_eq!(parsed, expected);
212    }
213
214    #[test]
215    fn broken_from_string() {
216        HostsFileLine::from_string("127.0.0.1").expect_err("should fail");
217    }
218    #[test]
219    fn host_from_string() {
220        let parsed = HostsFileLine::from_string("127.0.0.1 localhost").unwrap();
221        let expected = HostsFileLine {
222            is_empty: false,
223            ip: Some("127.0.0.1".to_string()),
224            hosts: Some(vec!["localhost".to_string()]),
225            comment: None,
226        };
227        assert_eq!(parsed, expected);
228    }
229    #[test]
230    fn full_from_string() {
231        let parsed = HostsFileLine::from_string("127.0.0.1 localhost  # a comment").unwrap();
232        let expected = HostsFileLine {
233            is_empty: false,
234            ip: Some("127.0.0.1".to_string()),
235            hosts: Some(vec!["localhost".to_string()]),
236            comment: Some("# a comment".to_string()),
237        };
238        assert_eq!(parsed, expected);
239    }
240    #[test]
241    fn empty_input() {
242        let parsed = HostsFile::from_str("").unwrap();
243        let expected = HostsFile { lines: vec![] };
244        assert_eq!(parsed, expected);
245    }
246    #[test]
247    fn a_comment() {
248        let parsed = HostsFile::from_str("# comment").unwrap();
249        let expected = HostsFile {
250            lines: vec![HostsFileLine::from_comment("# comment")],
251        };
252        assert_eq!(parsed, expected);
253    }
254    #[test]
255    fn two_comments() {
256        let parsed = HostsFile::from_str("# comment1\n## comment2\n").unwrap();
257        let expected = HostsFile {
258            lines: vec![
259                HostsFileLine::from_comment("# comment1"),
260                HostsFileLine::from_comment("## comment2"),
261            ],
262        };
263        assert_eq!(parsed, expected);
264    }
265    #[test]
266    fn host_with_comments() {
267        let parsed = HostsFile::from_str("127.0.0.1 localhost # comment\n").unwrap();
268        let expected = HostsFile {
269            lines: vec![HostsFileLine {
270                is_empty: false,
271                ip: Some("127.0.0.1".to_string()),
272                hosts: Some(vec!["localhost".to_string()]),
273                comment: Some("# comment".to_string()),
274            }],
275        };
276        assert_eq!(parsed, expected);
277    }
278    #[test]
279    fn whitespace() {
280        let parsed = HostsFile::from_str(" # comment1\n \n    127.0.0.1    localhost\n").unwrap();
281        let expected = HostsFile {
282            lines: vec![
283                HostsFileLine::from_comment("# comment1"),
284                HostsFileLine::from_empty(),
285                HostsFileLine::from_string("127.0.0.1 localhost").unwrap(),
286            ],
287        };
288        assert_eq!(parsed, expected);
289    }
290    #[test]
291    fn a_ipv6_host() {
292        let parsed = HostsFile::from_str("fe80::1%lo0 localhost\n").unwrap();
293        let expected = HostsFile {
294            lines: vec![HostsFileLine {
295                is_empty: false,
296                ip: Some("fe80::1%lo0".to_string()),
297                hosts: Some(vec!["localhost".to_string()]),
298                comment: None,
299            }],
300        };
301        assert_eq!(parsed, expected);
302    }
303
304    #[test]
305    fn a_ipv4_host() {
306        let parsed = HostsFile::from_str("127.0.0.1 localhost").unwrap();
307        let expected = HostsFile {
308            lines: vec![HostsFileLine {
309                is_empty: false,
310                ip: Some("127.0.0.1".to_string()),
311                hosts: Some(vec!["localhost".to_string()]),
312                comment: None,
313            }],
314        };
315        assert_eq!(parsed, expected);
316    }
317
318    #[test]
319    fn complex_1() {
320        let parsed = HostsFile::from_str("# A sample host file\n# empty line\n\n127.0.0.1 localhost\n# multiple hosts\n127.0.0.2 host1 host2\n").unwrap();
321        let expected = HostsFile {
322            lines: vec![
323                HostsFileLine::from_comment("# A sample host file"),
324                HostsFileLine::from_comment("# empty line"),
325                HostsFileLine::from_empty(),
326                HostsFileLine {
327                    is_empty: false,
328                    ip: Some("127.0.0.1".to_string()),
329                    hosts: Some(vec!["localhost".to_string()]),
330                    comment: None,
331                },
332                HostsFileLine::from_comment("# multiple hosts"),
333                HostsFileLine {
334                    is_empty: false,
335                    ip: Some("127.0.0.2".to_string()),
336                    hosts: Some(
337                        vec!["host1", "host2"]
338                            .iter()
339                            .map(|s| s.to_string())
340                            .collect(),
341                    ),
342                    comment: None,
343                },
344            ],
345        };
346        assert_eq!(parsed, expected);
347    }
348
349    // Serialize
350
351    #[test]
352    fn serialize_empty() {
353        let input = "\n";
354        let serialized = HostsFile::from_str(input).unwrap().serialize();
355        assert_eq!(serialized, input);
356    }
357    #[test]
358    fn serialize_a_comment() {
359        let input = "# a comment\n";
360        let serialized = HostsFile::from_str(input).unwrap().serialize();
361        assert_eq!(serialized, input);
362    }
363
364    #[test]
365    fn serialize_complex_1() {
366        let input = "# A sample host file\n# empty line\n\n127.0.0.1 localhost\n# multiple hosts\n127.0.0.2 host1 host2\n";
367        let serialized = HostsFile::from_str(input).unwrap().serialize();
368        assert_eq!(serialized, input);
369    }
370}