1use std::fs::File;
2use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
3use std::net::{AddrParseError, IpAddr};
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6
7fn parse_ip(input: &str) -> Result<(IpAddr, &str), AddrParseError> {
33 let non_ip_char_idx = input.find(|c: char| c != '.' && c != ':' && !c.is_digit(16));
34 let (ip, remainder) = input.split_at(non_ip_char_idx.unwrap_or(input.len()));
35 Ok((ip.parse()?, remainder))
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct HostEntry {
41 pub ip: IpAddr,
42 pub names: Vec<String>,
43}
44
45impl FromStr for HostEntry {
46 type Err = String;
47
48 fn from_str(s: &str) -> Result<Self, Self::Err> {
49 let mut input = s;
50 input = input.trim_start();
51
52 let ip =
53 parse_ip(input).map_err(|err| format!("Couldn't parse a valid IP address: {err}"))?;
54 input = ip.1;
55 let ip = ip.0;
56
57 match input.chars().next() {
58 Some(' ') | Some('\t') => {}
59 _ => {
60 return Err("Expected whitespace after IP".to_string());
61 }
62 }
63 input = input.trim_start();
64
65 let mut names = Vec::new();
66 for name in input.split_whitespace() {
67 match name.chars().next() {
69 Some('#') => break,
70 Some(_) => {}
71 None => unreachable!(),
72 }
73 names.push(name.to_string());
74 }
75
76 Ok(HostEntry { ip, names })
77 }
78}
79
80pub fn parse_file(path: &Path) -> Result<Vec<HostEntry>, String> {
82 if !path.exists() || !path.is_file() {
83 return Err(format!(
84 "File ({:?}) does not exist or is not a regular file",
85 path
86 ));
87 }
88
89 let mut file = File::open(path).map_err(|_x| format!("Could not open file ({:?})", path))?;
90 let mut buf = vec![0; 3];
91 let n = file
92 .read(&mut buf)
93 .map_err(|err| format!("Reading header failed {}", err))?;
94 if n == 3 {
95 if &buf != b"\xef\xbb\xbf" {
98 file.seek(SeekFrom::Start(0))
99 .map_err(|err| format!("Seek failed {}", err))?;
100 }
101 }
102
103 let mut entries = Vec::new();
104
105 let lines = BufReader::new(file).lines();
106 let mut line_count = 1;
107 for line in lines {
108 let line = line.map_err(|err| format!("Error reading file at line {line_count}: {err}"))?;
109 let line = line.trim_start();
110 match line.chars().next() {
111 Some('#') => continue,
113 None => continue,
115 Some(_) => {
117 entries.push(
118 line.parse().map_err(|err| {
119 format!("{err} at line {line_count} with content: '{line}'")
120 })?,
121 );
122 line_count += 1;
123 }
124 }
125 }
126
127 Ok(entries)
128}
129
130pub fn parse_hostfile() -> Result<Vec<HostEntry>, String> {
135 parse_file(&get_hostfile_path()?)
136}
137
138pub fn get_hostfile_path() -> Result<PathBuf, String> {
140 #[cfg(not(windows))]
141 {
142 Ok(PathBuf::from("/etc/hosts"))
143 }
144
145 #[cfg(windows)]
146 {
147 use std::ffi::OsString;
150 use std::os::windows::ffi::OsStringExt;
151 use std::ptr::null_mut;
152 use std::slice;
153 use windows_sys::Win32::{
154 Foundation::S_OK,
155 System::Com::CoTaskMemFree,
156 UI::Shell::{FOLDERID_System, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY},
157 };
158
159 extern "C" {
160 fn wcslen(buf: *const u16) -> usize;
161 }
162
163 let mut ptr = null_mut::<u16>();
164 let ret = unsafe {
165 SHGetKnownFolderPath(
166 &FOLDERID_System,
167 KF_FLAG_DONT_VERIFY as u32,
168 null_mut(),
169 &mut ptr,
170 )
171 };
172
173 match ret {
174 S_OK => {
175 let path_slice = unsafe { slice::from_raw_parts(ptr, wcslen(ptr)) };
176 let os_str = OsString::from_wide(path_slice);
177 unsafe { CoTaskMemFree(ptr.cast()) };
178 let mut pathbuf = PathBuf::from(&os_str);
179 pathbuf.push("drivers\\etc\\hosts");
180 Ok(pathbuf)
181 }
182 _ => {
183 unsafe { CoTaskMemFree(ptr.cast()) };
185 Err(format!(
186 "Could not get path to Windows hosts file: {}",
187 std::io::Error::last_os_error(),
188 ))
189 }
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 extern crate mktemp;
197 use mktemp::Temp;
198
199 use std::io::{Seek, SeekFrom, Write};
200 use std::net::{Ipv4Addr, Ipv6Addr};
201
202 use super::*;
203
204 #[test]
205 fn parse_ipv4() {
206 let input = "127.0.0.1";
207 assert_eq!(
208 parse_ip(input),
209 Ok((IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), ""))
210 );
211 }
212
213 #[test]
214 fn parse_ipv6() {
215 let input = "::1";
216 assert_eq!(
217 parse_ip(input),
218 Ok((IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), ""))
219 );
220 }
221
222 #[test]
223 fn parse_entry() {
224 assert_eq!(
225 "127.0.0.1 localhost".parse(),
226 Ok(HostEntry {
227 ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
228 names: vec!(String::from("localhost")),
229 })
230 );
231 }
232
233 #[test]
234 fn parse_entry_multiple_names() {
235 assert_eq!(
236 "127.0.0.1 localhost home ".parse(),
237 Ok(HostEntry {
238 ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
239 names: vec!(String::from("localhost"), String::from("home")),
240 })
241 );
242 }
243
244 #[test]
245 fn parse_entry_ipv6() {
246 assert_eq!(
247 "::1 localhost".parse(),
248 Ok(HostEntry {
249 ip: IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
250 names: vec!(String::from("localhost")),
251 })
252 );
253 }
254
255 #[test]
256 fn parse_entry_with_ws_and_comments() {
257 assert_eq!(
258 " ::1 \tlocalhost # comment".parse(),
259 Ok(HostEntry {
260 ip: IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
261 names: vec!(String::from("localhost")),
262 })
263 );
264 }
265
266 #[test]
267 fn test_parse_file() {
268 let temp_file = Temp::new_file().unwrap();
269 let temp_path = temp_file.as_path();
270 let mut file = File::create(temp_path).unwrap();
271
272 write!(
273 file,
274 "\
275 # This is a sample hosts file\n\
276 \n# Sometimes hosts files can have wonky spacing
277 127.0.0.1 localhost\n\
278 ::1 localhost\n\
279 255.255.255.255 broadcast\n\
280 \n\
281 # Comments can really be anywhere\n\
282 bad:dad::ded multiple hostnames for address\n\
283 1.1.1.1\ttabSeperatedHostname\n\
284 1.1.1.2\t tabAndSpaceSeparatedHostName\n\
285 \t1.1.1.3\t\t\tlineStartsWithTab\n\
286 1.1.1.4 lineStartsWithSpace\n\
287 1.1.1.5 skip_blank_line
288 "
289 )
290 .expect("Could not write to temp file");
291
292 assert_eq!(
293 parse_file(&temp_path),
294 Ok(vec!(
295 HostEntry {
296 ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
297 names: vec!(String::from("localhost")),
298 },
299 HostEntry {
300 ip: IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
301 names: vec!(String::from("localhost")),
302 },
303 HostEntry {
304 ip: IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255)),
305 names: vec!(String::from("broadcast")),
306 },
307 HostEntry {
308 ip: IpAddr::V6(Ipv6Addr::new(0xbad, 0xdad, 0, 0, 0, 0, 0, 0xded)),
309 names: vec!(
310 String::from("multiple"),
311 String::from("hostnames"),
312 String::from("for"),
313 String::from("address")
314 ),
315 },
316 HostEntry {
317 ip: IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
318 names: vec!(String::from("tabSeperatedHostname")),
319 },
320 HostEntry {
321 ip: IpAddr::V4(Ipv4Addr::new(1, 1, 1, 2)),
322 names: vec!(String::from("tabAndSpaceSeparatedHostName")),
323 },
324 HostEntry {
325 ip: IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
326 names: vec!(String::from("lineStartsWithTab")),
327 },
328 HostEntry {
329 ip: IpAddr::V4(Ipv4Addr::new(1, 1, 1, 4)),
330 names: vec!(String::from("lineStartsWithSpace")),
331 },
332 HostEntry {
333 ip: IpAddr::V4(Ipv4Addr::new(1, 1, 1, 5)),
334 names: vec!(String::from("skip_blank_line")),
335 },
336 ))
337 );
338 }
339
340 #[test]
341 fn test_parse_file_with_bom() {
342 let temp_file = Temp::new_file().unwrap();
343 let temp_path = temp_file.as_path();
344 let mut file = File::create(temp_path).unwrap();
345 file.write_all(b"\xef\xbb\xbf\n127.0.0.1 localhost\n")
346 .expect("Could not write to temp file");
347 parse_file(&temp_path).unwrap();
348 }
349
350 #[test]
351 fn test_parse_file_errors() {
352 let temp_file = Temp::new_file().unwrap();
353 let temp_path = temp_file.as_path();
354 let mut file = File::create(temp_path).unwrap();
355
356 write!(file, "127.0.0.1localhost\n").expect("");
357 assert_eq!(
358 parse_file(&temp_path),
359 Err(
360 "Expected whitespace after IP at line 1 with content: '127.0.0.1localhost'"
361 .to_string()
362 )
363 );
364
365 file.set_len(0).expect("Could not truncate file");
366 file.seek(SeekFrom::Start(0)).expect("");
367 write!(file, "127.0.0 localhost\n").expect("");
368 assert_eq!(
369 parse_file(&temp_path),
370 Err("Couldn't parse a valid IP address: invalid IP address syntax at line 1 with content: '127.0.0 localhost'".to_string())
371 );
372
373 file.set_len(0).expect("");
374 file.seek(SeekFrom::Start(0)).expect("");
375 write!(file, "127.0.0 local\nhost\n").expect("");
376 assert_eq!(
377 parse_file(&temp_path),
378 Err("Couldn't parse a valid IP address: invalid IP address syntax at line 1 with content: '127.0.0 local'".to_string())
379 );
380
381 file.set_len(0).expect("");
382 file.seek(SeekFrom::Start(0)).expect("");
383 write!(file, "127.0.0.1 localhost\nlocalhost myhost").expect("");
384 assert_eq!(
385 parse_file(&temp_path),
386 Err("Couldn't parse a valid IP address: invalid IP address syntax at line 2 with content: 'localhost myhost'".to_string())
387 );
388
389 let temp_dir = Temp::new_dir().unwrap();
390 let temp_dir_path = temp_dir.as_path();
391 assert_eq!(
392 parse_file(&temp_dir_path),
393 Err(format!(
394 "File ({:?}) does not exist or is not a regular file",
395 temp_dir_path
396 ))
397 );
398 }
399
400 #[test]
401 fn test_clone() {
402 let host_entry = HostEntry {
403 ip: IpAddr::V4(Ipv4Addr::new(192, 168, 42, 42)),
404 names: vec![String::from("comp1"), String::from("computer1")],
405 };
406 let cloned = host_entry.clone();
407 assert_eq!(host_entry, cloned)
408 }
409
410 #[test]
411 fn test_get_hostfile_path() {
412 let maybe_path = get_hostfile_path();
413 assert!(maybe_path.is_ok());
414 assert!(maybe_path.unwrap().exists());
415 }
416
417 #[test_with::env(GITHUB_ACTIONS)]
425 #[test]
426 fn test_parse_hostfile() {
427 let maybe_hostfile = parse_hostfile();
428 assert!(maybe_hostfile.is_ok());
429 let hostfile = maybe_hostfile.unwrap();
430 assert!(!hostfile.is_empty());
431
432 #[cfg(not(windows))]
433 {
434 let localhost = hostfile
435 .iter()
436 .find(|entry| entry.names.contains(&String::from("localhost")));
437 assert!(localhost.is_some());
438 }
439 }
440}