servicefile/
lib.rs

1use std::fs::File;
2use std::io::{BufRead, BufReader};
3use std::path::Path;
4use std::str::FromStr;
5
6/**
7 * service file format:
8 *   File:
9 *     Line |
10 *     Line newline File
11 *
12 *   Line:
13 *     Comment | Empty | Entry
14 *
15 *   Comment:
16 *     # .* newline
17 *
18 *   Entry:
19 *     ws* servicename ws+ port/protocol aliases
20 */
21
22fn discard_ws(input: &str, start_idx: usize) -> usize {
23    let mut chars = input[start_idx..].chars();
24    let mut end_idx = start_idx;
25
26    loop {
27        let c = chars.next();
28        if c.is_none() || !c.unwrap().is_whitespace() {
29            break;
30        }
31
32        end_idx += 1;
33    }
34
35    end_idx
36}
37
38/// A struct representing a line from /etc/services that has a service on it
39#[derive(Debug, PartialEq)]
40pub struct ServiceEntry {
41    pub name: String,
42    pub port: usize,
43    pub protocol: String,
44    pub aliases: Vec<String>,
45}
46
47fn is_comment(s: &str) -> bool {
48    if let Some(c) = s.chars().next() {
49        return c == '#';
50    }
51
52    false
53}
54
55impl FromStr for ServiceEntry {
56    type Err = &'static str;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        let mut service = s.split_whitespace();
60
61        let name = service.next();
62        let name = name.unwrap().to_string();
63        if is_comment(&name) {
64            return Err("Malformed input");
65        }
66
67        let port_and_protocol = service.next();
68        if port_and_protocol.is_none() {
69            return Err("Could not find port and protocol field");
70        }
71        let mut port_and_protocol = port_and_protocol.unwrap().split("/");
72
73        let port = port_and_protocol.next().unwrap();
74        if is_comment(port) {
75            return Err("Could not find port and protocol field");
76        }
77        let port = port.parse::<usize>();
78        if port.is_err() {
79            return Err("Malformed port");
80        }
81        let port = port.unwrap();
82
83        let protocol = port_and_protocol.next();
84        if protocol.is_none() {
85            return Err("Could not find protocol");
86        }
87        let protocol = protocol.unwrap().to_string();
88        if is_comment(&protocol) {
89            return Err("Could not find protocol");
90        }
91
92        let mut aliases = Vec::new();
93        for alias in service {
94            if let Some(c) = alias.chars().next() {
95                if c == '#' {
96                    break;
97                }
98            }
99
100            aliases.push(alias.to_string());
101        }
102
103        Ok(ServiceEntry {
104            name,
105            port,
106            protocol,
107            aliases,
108        })
109    }
110}
111
112/// Parse a file using the format described in `man services(5)`
113/// if ignore_errs is true, then all parsing errors will be ignored. This is needed on some systems
114/// which don't entirely respect the format in services(5) and omit a service name
115pub fn parse_file(path: &Path, ignore_errs: bool) -> Result<Vec<ServiceEntry>, &'static str> {
116    if !path.exists() || !path.is_file() {
117        return Err("File does not exist or is not a regular file");
118    }
119
120    let file = File::open(path);
121    if file.is_err() {
122        return Err("Could not open file");
123    }
124    let file = file.unwrap();
125
126    let mut entries = Vec::new();
127
128    let lines = BufReader::new(file).lines();
129    for line in lines {
130        if let Err(_) = line {
131            return Err("Error reading file");
132        }
133        let line = line.unwrap();
134
135        let start = discard_ws(&line, 0);
136        let entryline = &line[start..];
137        match entryline.chars().next() {
138            Some(c) => {
139                if c == '#' {
140                    continue;
141                }
142            }
143            // empty line
144            None => {
145                continue;
146            }
147        };
148
149        match entryline.parse() {
150            Ok(entry) => {
151                entries.push(entry);
152            }
153            Err(msg) => {
154                if !ignore_errs {
155                    return Err(msg);
156                }
157            }
158        };
159    }
160
161    Ok(entries)
162}
163
164/// Parse /etc/services
165pub fn parse_servicefile(ignore_errs: bool) -> Result<Vec<ServiceEntry>, &'static str> {
166    parse_file(&Path::new("/etc/services"), ignore_errs)
167}
168
169#[cfg(test)]
170mod tests {
171    extern crate mktemp;
172    use mktemp::Temp;
173
174    use std::io::{Seek, SeekFrom, Write};
175
176    use super::*;
177
178    #[test]
179    fn parse_entry() {
180        assert_eq!(
181            "tcpmux            1/tcp     # TCP Port Service Multiplexer".parse(),
182            Ok(ServiceEntry {
183                name: "tcpmux".to_string(),
184                port: 1,
185                protocol: "tcp".to_string(),
186                aliases: vec!(),
187            })
188        );
189    }
190
191    #[test]
192    fn parse_entry_multiple_aliases() {
193        assert_eq!(
194            "tcpmux 1/tcp tcpmultiplexer niceservice".parse(),
195            Ok(ServiceEntry {
196                name: "tcpmux".to_string(),
197                port: 1,
198                protocol: "tcp".to_string(),
199                aliases: vec!("tcpmultiplexer".to_string(), "niceservice".to_string()),
200            })
201        );
202    }
203
204    #[test]
205    fn test_parse_file() {
206        let temp_file = Temp::new_file().unwrap();
207        let temp_path = temp_file.as_path();
208        let mut file = File::create(temp_path).unwrap();
209
210        write!(
211            file,
212            "\
213                # WELL KNOWN PORT NUMBERS\n\
214                #
215                rtmp              1/ddp    #Routing Table Maintenance Protocol\n\
216                tcpmux            1/udp     # TCP Port Service Multiplexer\n\
217                tcpmux            1/tcp     # TCP Port Service Multiplexer\n\
218                #                          Mark Lottor <MKL@nisc.sri.com>\n\
219                nbp               2/ddp    #Name Binding Protocol\n\
220                compressnet       2/udp     # Management Utility\n\
221                compressnet       2/tcp     # Management Utility\n\
222                compressnet       3/udp     # Compression Process\n\
223                compressnet       3/tcp     # Compression Process\n\
224            "
225        )
226        .expect("Could not write to temp file");
227        assert_eq!(
228            parse_file(&temp_path, false),
229            Ok(vec!(
230                ServiceEntry {
231                    name: "rtmp".to_string(),
232                    port: 1,
233                    protocol: "ddp".to_string(),
234                    aliases: vec!(),
235                },
236                ServiceEntry {
237                    name: "tcpmux".to_string(),
238                    port: 1,
239                    protocol: "udp".to_string(),
240                    aliases: vec!(),
241                },
242                ServiceEntry {
243                    name: "tcpmux".to_string(),
244                    port: 1,
245                    protocol: "tcp".to_string(),
246                    aliases: vec!(),
247                },
248                ServiceEntry {
249                    name: "nbp".to_string(),
250                    port: 2,
251                    protocol: "ddp".to_string(),
252                    aliases: vec!(),
253                },
254                ServiceEntry {
255                    name: "compressnet".to_string(),
256                    port: 2,
257                    protocol: "udp".to_string(),
258                    aliases: vec!(),
259                },
260                ServiceEntry {
261                    name: "compressnet".to_string(),
262                    port: 2,
263                    protocol: "tcp".to_string(),
264                    aliases: vec!(),
265                },
266                ServiceEntry {
267                    name: "compressnet".to_string(),
268                    port: 3,
269                    protocol: "udp".to_string(),
270                    aliases: vec!(),
271                },
272                ServiceEntry {
273                    name: "compressnet".to_string(),
274                    port: 3,
275                    protocol: "tcp".to_string(),
276                    aliases: vec!(),
277                },
278            ))
279        );
280    }
281
282    #[test]
283    fn test_parse_file_errors() {
284        let temp_file = Temp::new_file().unwrap();
285        let temp_path = temp_file.as_path();
286        let mut file = File::create(temp_path).unwrap();
287
288        write!(file, "service\n").expect("");
289        assert_eq!(
290            parse_file(&temp_path, false),
291            Err("Could not find port and protocol field")
292        );
293
294        file.set_len(0).expect("");
295        file.seek(SeekFrom::Start(0)).expect("");
296        write!(file, "service # 1/tcp\n").expect("");
297        assert_eq!(
298            parse_file(&temp_path, false),
299            Err("Could not find port and protocol field")
300        );
301
302        file.set_len(0).expect("");
303        file.seek(SeekFrom::Start(0)).expect("");
304        write!(file, "service  1#/tcp\n").expect("");
305        assert_eq!(parse_file(&temp_path, false), Err("Malformed port"));
306
307        file.set_len(0).expect("");
308        file.seek(SeekFrom::Start(0)).expect("");
309        write!(file, "service  1/#tcp\n").expect("");
310        assert_eq!(
311            parse_file(&temp_path, false),
312            Err("Could not find protocol")
313        );
314
315        file.set_len(0).expect("");
316        file.seek(SeekFrom::Start(0)).expect("");
317        write!(file, "service asdf/tcp\n").expect("");
318        assert_eq!(parse_file(&temp_path, false), Err("Malformed port"));
319
320        file.set_len(0).expect("");
321        file.seek(SeekFrom::Start(0)).expect("");
322        write!(file, "service asdf/\n").expect("");
323        assert_eq!(parse_file(&temp_path, false), Err("Malformed port"));
324
325        let temp_dir = Temp::new_dir().unwrap();
326        let temp_dir_path = temp_dir.as_path();
327        assert_eq!(
328            parse_file(&temp_dir_path, false),
329            Err("File does not exist or is not a regular file")
330        );
331    }
332
333    #[test]
334    fn test_parse_servicefile() {
335        assert_eq!(parse_servicefile(true).is_ok(), true);
336    }
337}