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 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 #[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 #[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}