1use crate::lex::Lex;
4use std::collections::HashMap;
5
6#[derive(Debug)]
7pub struct ParsingError {
8 lineno: u32,
9 message: String,
10}
11
12impl std::fmt::Display for ParsingError {
13 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 write!(f, "parsing error: {} (line {})", self.message, self.lineno)
15 }
16}
17
18#[derive(Debug, PartialEq, Eq, Clone, Default)]
20pub struct Authenticator {
21 pub login: String,
23
24 pub account: String,
26
27 pub password: String,
29}
30
31impl Authenticator {
32 #[allow(dead_code)]
33 pub fn new(login: &str, account: &str, password: &str) -> Self {
34 Authenticator {
35 login: login.to_owned(),
36 account: account.to_owned(),
37 password: password.to_owned(),
38 }
39 }
40}
41
42#[derive(Debug, Default)]
44pub struct Netrc {
45 pub hosts: HashMap<String, Authenticator>,
47
48 pub macros: HashMap<String, Vec<String>>,
50}
51
52impl std::fmt::Display for Netrc {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 let mut rep = String::new();
55 for (host, attrs) in self.hosts.iter() {
56 rep.push_str(&format!("machine {}\n\tlogin {}\n", host, attrs.login));
57 if !attrs.account.is_empty() {
58 rep.push_str(&format!("\taccount {}\n", attrs.account));
59 }
60 rep.push_str(&format!("\tpassword {}\n", attrs.password));
61 }
62 for (macro_, lines) in self.macros.iter() {
63 rep.push_str(&format!("macdef {}\n", macro_));
64 for line in lines.iter() {
65 rep.push_str(&format!("{}\n", line));
66 }
67 }
68 write!(f, "{}", rep)
69 }
70}
71
72impl std::str::FromStr for Netrc {
73 type Err = ParsingError;
74
75 fn from_str(s: &str) -> Result<Self, ParsingError> {
76 let mut res = Netrc::default();
77 let mut lexer = Lex::new(s);
78
79 loop {
80 let saved_lineno = lexer.lineno;
81 let tt = lexer.get_token();
82 if tt.is_empty() {
83 break;
84 }
85 if tt.chars().nth(0) == Some('#') {
86 if lexer.lineno == saved_lineno && tt.len() == 1 {
87 lexer.read_line();
88 }
89 continue;
90 }
91
92 #[allow(clippy::needless_late_init)]
93 let entryname;
94 match tt.as_str() {
95 "" => {
96 break;
97 }
98 "machine" => {
99 entryname = lexer.get_token();
100 }
101 "default" => {
102 entryname = String::from("default");
103 }
104 "macdef" => {
105 entryname = lexer.get_token();
106 let mut v = Vec::new();
107 loop {
108 let line = lexer.read_line();
109 if line.trim().is_empty() {
110 break;
111 }
112 v.push(line.trim().to_owned());
113 }
114 res.macros.insert(entryname, v);
115 continue;
116 }
117 _ => {
118 return Err(ParsingError {
119 lineno: lexer.lineno,
120 message: format!("bad toplevel token '{}'", tt),
121 });
122 }
123 };
124 if entryname.is_empty() {
125 return Err(ParsingError {
126 lineno: lexer.lineno,
127 message: format!("missing '{}' name", tt),
128 });
129 }
130
131 let mut auth = Authenticator::default();
132
133 loop {
134 let prev_lineno = lexer.lineno;
135 let tt = lexer.get_token();
136 if tt.starts_with('#') {
137 if lexer.lineno == prev_lineno {
138 lexer.read_line();
139 }
140 continue;
141 }
142 match tt.as_str() {
143 "" | "machine" | "default" | "macdef" => {
144 res.hosts.insert(entryname, auth);
145 lexer.push_token(&tt);
146 break;
147 }
148 "login" | "user" => {
149 auth.login = lexer.get_token();
150 }
151 "account" => {
152 auth.account = lexer.get_token();
153 }
154 "password" => {
155 auth.password = lexer.get_token();
156 }
157 _ => {
158 return Err(ParsingError {
159 lineno: lexer.lineno,
160 message: format!("bad follower token '{}'", tt),
161 });
162 }
163 };
164 }
165 }
166
167 Ok(res)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use std::str::FromStr;
174
175 use super::*;
176
177 #[test]
178 fn test_toplevel_non_ordered_tokens() {
179 let nrc = Netrc::from_str(
180 "\
181 machine host.domain.com password pass1 login log1 account acct1
182 default login log2 password pass2 account acct2
183 ",
184 )
185 .unwrap();
186
187 assert_eq!(
188 nrc.hosts["host.domain.com"],
189 Authenticator::new("log1", "acct1", "pass1")
190 );
191 assert_eq!(
192 nrc.hosts["default"],
193 Authenticator::new("log2", "acct2", "pass2")
194 );
195 }
196
197 #[test]
198 fn test_toplevel_tokens() {
199 let nrc = Netrc::from_str(
200 "\
201 machine host.domain.com login log1 password pass1 account acct1
202 default login log2 password pass2 account acct2
203 ",
204 )
205 .unwrap();
206 assert_eq!(
207 nrc.hosts["host.domain.com"],
208 Authenticator::new("log1", "acct1", "pass1")
209 );
210 assert_eq!(
211 nrc.hosts["default"],
212 Authenticator::new("log2", "acct2", "pass2")
213 );
214 }
215
216 #[test]
217 fn test_macros() {
218 let nrc = Netrc::from_str(
219 "\
220 macdef macro1
221 line1
222 line2
223
224 macdef macro2
225 line3
226 line4
227 ",
228 )
229 .unwrap();
230 assert_eq!(nrc.macros["macro1"], vec!["line1", "line2"]);
231 assert_eq!(nrc.macros["macro2"], vec!["line3", "line4"]);
232 }
233
234 #[test]
235 fn test_optional_tokens_machine() {
236 let data = vec![
237 "machine host.domain.com",
238 "machine host.domain.com login",
239 "machine host.domain.com account",
240 "machine host.domain.com password",
241 "machine host.domain.com login \"\" account",
242 "machine host.domain.com login \"\" password",
243 "machine host.domain.com account \"\" password",
244 ];
245
246 for item in data {
247 let nrc = Netrc::from_str(item).unwrap();
248 assert_eq!(nrc.hosts["host.domain.com"], Authenticator::new("", "", ""));
249 }
250 }
251
252 #[test]
253 fn test_optional_tokens_default() {
254 let data = vec![
255 "default",
256 "default login",
257 "default account",
258 "default password",
259 "default login \"\" account",
260 "default login \"\" password",
261 "default account \"\" password",
262 ];
263
264 for item in data {
265 let nrc = Netrc::from_str(item).unwrap();
266 assert_eq!(nrc.hosts["default"], Authenticator::new("", "", ""));
267 }
268 }
269
270 #[test]
271 fn test_invalid_tokens() {
272 let data = vec![
273 (
274 "invalid host.domain.com",
275 "parsing error: bad toplevel token 'invalid' (line 1)",
276 ),
277 (
278 "machine host.domain.com invalid",
279 "parsing error: bad follower token 'invalid' (line 1)",
280 ),
281 (
282 "machine host.domain.com login log password pass account acct invalid",
283 "parsing error: bad follower token 'invalid' (line 1)",
284 ),
285 (
286 "default host.domain.com invalid",
287 "parsing error: bad follower token 'host.domain.com' (line 1)",
288 ),
289 (
290 "default host.domain.com login log password pass account acct invalid",
291 "parsing error: bad follower token 'host.domain.com' (line 1)",
292 ),
293 ];
294
295 for (item, msg) in data {
296 let nrc = Netrc::from_str(item);
297 assert_eq!(nrc.unwrap_err().to_string(), msg);
298 }
299 }
300
301 fn test_token_x(data: &str, token: &str, value: &str) {
302 let nrc = Netrc::from_str(data).unwrap();
303 match token {
304 "login" => {
305 assert_eq!(
306 nrc.hosts["host.domain.com"],
307 Authenticator::new(value, "acct", "pass")
308 );
309 }
310 "account" => {
311 assert_eq!(
312 nrc.hosts["host.domain.com"],
313 Authenticator::new("log", value, "pass")
314 );
315 }
316 "password" => {
317 assert_eq!(
318 nrc.hosts["host.domain.com"],
319 Authenticator::new("log", "acct", value)
320 );
321 }
322 _ => {}
323 };
324 }
325
326 #[test]
327 fn test_token_value_quotes() {
328 test_token_x(
329 "\
330 machine host.domain.com login \"log\" password pass account acct
331 ",
332 "login",
333 "log",
334 );
335 test_token_x(
336 "\
337 machine host.domain.com login log password pass account \"acct\"
338 ",
339 "account",
340 "acct",
341 );
342 test_token_x(
343 "\
344 machine host.domain.com login log password \"pass\" account acct
345 ",
346 "password",
347 "pass",
348 );
349 }
350
351 #[test]
352 fn test_token_value_escape() {
353 test_token_x(
354 r#"machine host.domain.com login \"log password pass account acct"#,
355 "login",
356 "\"log",
357 );
358 test_token_x(
359 "\
360 machine host.domain.com login \"\\\"log\" password pass account acct
361 ",
362 "login",
363 "\"log",
364 );
365 test_token_x(
366 "\
367 machine host.domain.com login log password pass account \\\"acct
368 ",
369 "account",
370 "\"acct",
371 );
372 test_token_x(
373 "\
374 machine host.domain.com login log password pass account \"\\\"acct\"
375 ",
376 "account",
377 "\"acct",
378 );
379 test_token_x(
380 "\
381 machine host.domain.com login log password \\\"pass account acct
382 ",
383 "password",
384 "\"pass",
385 );
386 test_token_x(
387 "\
388 machine host.domain.com login log password \"\\\"pass\" account acct
389 ",
390 "password",
391 "\"pass",
392 );
393 }
394
395 #[test]
396 fn test_token_value_whitespace() {
397 test_token_x(
398 r#"machine host.domain.com login "lo g" password pass account acct"#,
399 "login",
400 "lo g",
401 );
402 test_token_x(
403 r#"machine host.domain.com login log password "pas s" account acct"#,
404 "password",
405 "pas s",
406 );
407 test_token_x(
408 r#"machine host.domain.com login log password pass account "acc t""#,
409 "account",
410 "acc t",
411 );
412 }
413
414 #[test]
415 fn test_token_value_non_ascii() {
416 test_token_x(
417 r#"machine host.domain.com login ¡¢ password pass account acct"#,
418 "login",
419 "¡¢",
420 );
421 test_token_x(
422 r#"machine host.domain.com login log password pass account ¡¢"#,
423 "account",
424 "¡¢",
425 );
426 test_token_x(
427 r#"machine host.domain.com login log password ¡¢ account acct"#,
428 "password",
429 "¡¢",
430 );
431 }
432
433 #[test]
434 fn test_token_value_leading_hash() {
435 test_token_x(
436 r#"machine host.domain.com login #log password pass account acct"#,
437 "login",
438 "#log",
439 );
440 test_token_x(
441 r#"machine host.domain.com login log password pass account #acct"#,
442 "account",
443 "#acct",
444 );
445 test_token_x(
446 r#"machine host.domain.com login log password #pass account acct"#,
447 "password",
448 "#pass",
449 );
450 }
451
452 #[test]
453 fn test_token_value_trailing_hash() {
454 test_token_x(
455 r#"machine host.domain.com login log# password pass account acct"#,
456 "login",
457 "log#",
458 );
459 test_token_x(
460 r#"machine host.domain.com login log password pass account acct#"#,
461 "account",
462 "acct#",
463 );
464 test_token_x(
465 r#"machine host.domain.com login log password pass# account acct"#,
466 "password",
467 "pass#",
468 );
469 }
470
471 #[test]
472 fn test_token_value_internal_hash() {
473 test_token_x(
474 r#"machine host.domain.com login lo#g password pass account acct"#,
475 "login",
476 "lo#g",
477 );
478 test_token_x(
479 r#"machine host.domain.com login log password pass account ac#ct"#,
480 "account",
481 "ac#ct",
482 );
483 test_token_x(
484 r#"machine host.domain.com login log password pa#ss account acct"#,
485 "password",
486 "pa#ss",
487 );
488 }
489
490 fn test_comment(data: &str) {
491 let nrc = Netrc::from_str(data).unwrap();
492 assert_eq!(
493 nrc.hosts["foo.domain.com"],
494 Authenticator::new("bar", "", "pass")
495 );
496 assert_eq!(
497 nrc.hosts["bar.domain.com"],
498 Authenticator::new("foo", "", "pass")
499 );
500 }
501
502 #[test]
503 fn test_comment_before_machine_line() {
504 test_comment(
505 r#"# comment
506 machine foo.domain.com login bar password pass
507 machine bar.domain.com login foo password pass
508 "#,
509 );
510 }
511 #[test]
512 fn test_comment_before_machine_line_no_space() {
513 test_comment(
514 r#"#comment
515 machine foo.domain.com login bar password pass
516 machine bar.domain.com login foo password pass
517 "#,
518 );
519 }
520
521 #[test]
522 fn test_comment_before_machine_line_hash_only() {
523 test_comment(
524 r#"#
525 machine foo.domain.com login bar password pass
526 machine bar.domain.com login foo password pass
527 "#,
528 );
529 }
530
531 #[test]
532 fn test_comment_after_machine_line() {
533 test_comment(
534 r#"machine foo.domain.com login bar password pass
535 # comment
536 machine bar.domain.com login foo password pass
537 "#,
538 );
539 test_comment(
540 r#"machine foo.domain.com login bar password pass
541 machine bar.domain.com login foo password pass
542 # comment
543 "#,
544 );
545 }
546
547 #[test]
548 fn test_comment_after_machine_line_no_space() {
549 test_comment(
550 r#"machine foo.domain.com login bar password pass
551 #comment
552 machine bar.domain.com login foo password pass
553 "#,
554 );
555 test_comment(
556 r#"machine foo.domain.com login bar password pass
557 machine bar.domain.com login foo password pass
558 #comment
559 "#,
560 );
561 }
562
563 #[test]
564 fn test_comment_after_machine_line_hash_only() {
565 test_comment(
566 r#"machine foo.domain.com login bar password pass
567 #
568 machine bar.domain.com login foo password pass
569 "#,
570 );
571 test_comment(
572 r#"machine foo.domain.com login bar password pass
573 machine bar.domain.com login foo password pass
574 #
575 "#,
576 );
577 }
578
579 #[test]
580 fn test_comment_at_end_of_machine_line() {
581 test_comment(
582 r#"machine foo.domain.com login bar password pass # comment
583 machine bar.domain.com login foo password pass
584 "#,
585 );
586 }
587
588 #[test]
589 fn test_comment_at_end_of_machine_line_no_space() {
590 test_comment(
591 r#"machine foo.domain.com login bar password pass #comment
592 machine bar.domain.com login foo password pass
593 "#,
594 );
595 }
596
597 #[test]
598 fn test_comment_at_end_of_machine_line_pass_has_hash() {
599 let nrc = Netrc::from_str(
600 r#"machine foo.domain.com login bar password #pass #comment
601 machine bar.domain.com login foo password pass
602 "#,
603 )
604 .unwrap();
605 assert_eq!(
606 nrc.hosts["foo.domain.com"],
607 Authenticator::new("bar", "", "#pass")
608 );
609 assert_eq!(
610 nrc.hosts["bar.domain.com"],
611 Authenticator::new("foo", "", "pass")
612 );
613 }
614}