1use std::fs::File;
2use std::io::{BufRead, BufReader};
3use std::path::Path;
4use std::str::FromStr;
5
6fn 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#[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
112pub 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 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
164pub 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}