netrc/
netrc.rs

1//! This parser and the tests are a translation of the official Python netrc library.
2
3use 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/// Authenticators for host.
19#[derive(Debug, PartialEq, Eq, Clone, Default)]
20pub struct Authenticator {
21    /// Identify a user on the remote machine.
22    pub login: String,
23
24    /// Supply an additional account password.
25    pub account: String,
26
27    /// Supply a password
28    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/// Represents the netrc file.
43#[derive(Debug, Default)]
44pub struct Netrc {
45    /// Dictionary mapping host names to the authentificators.
46    pub hosts: HashMap<String, Authenticator>,
47
48    /// Dictionary mapping macro names to string lists.
49    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}