netrc_rs/
lib.rs

1#[cfg(not(feature = "std"))]
2extern crate alloc;
3#[cfg(feature = "std")]
4extern crate std;
5
6#[cfg(not(feature = "std"))]
7use alloc::string::{String, ToString};
8#[cfg(not(feature = "std"))]
9use alloc::vec::Vec;
10use core::fmt;
11use core::fmt::{Display, Formatter};
12use core::result;
13use core::str::Chars;
14
15/// The `.netrc` machine info
16#[derive(Debug, Default, Eq, PartialEq, Clone)]
17pub struct Machine {
18    /// Identify a remote machine name, None is `default`
19    pub name: Option<String>,
20    /// Identify a user on the remote machine
21    pub login: Option<String>,
22    /// a password
23    pub password: Option<String>,
24    /// an additional account password
25    pub account: Option<String>,
26}
27
28impl Display for Machine {
29    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30        macro_rules! write_key {
31            ($key:expr, $fmt:expr, $default:expr) => {
32                match &$key {
33                    None => write!(f, $default),
34                    Some(val) => write!(f, $fmt, val),
35                }
36            };
37        }
38
39        write_key!(self.name, "machine {}", "default")?;
40        write_key!(self.login, " login {}", "")?;
41        write_key!(self.password, " password {}", "")?;
42        write_key!(self.account, " account {}", "")?;
43
44        Ok(())
45    }
46}
47
48/// Netrc represents a `.netrc` file struct
49#[derive(Debug, Default)]
50pub struct Netrc {
51    /// machine name and one machine info are paired
52    pub machines: Vec<Machine>,
53    /// macro name and a list cmd are paired
54    pub macdefs: Vec<(String, Vec<String>)>,
55    /// support collecting unknown entry when parsing for extended using
56    pub unknown_entries: Vec<String>,
57}
58
59/// Position saves row and column number, index is starting from 1
60#[derive(Debug, Copy, Clone)]
61pub struct Position(pub usize, pub usize);
62
63/// Error occurs when parsing `.netrc` text
64#[derive(Debug)]
65pub enum Error {
66    /// EOF occurs when read unexpected eof
67    EOF,
68    /// IllegalFormat occurs when meet mistake format
69    IllegalFormat(Position, String),
70}
71
72impl Display for Error {
73    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
74        match self {
75            Error::EOF => write!(f, "End of data: EOF"),
76            Error::IllegalFormat(pos, s) => write!(f, "Illegal format in {} {}", pos, s.as_str()),
77        }
78    }
79}
80
81pub type Result<T> = result::Result<T, Error>;
82
83impl Netrc {
84    /// Parse a `Netrc` format str.
85    /// If pass true to `unknown_entries`, it will collect unknown entries.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use netrc_rs::{Netrc, Machine};
91    ///
92    /// let input = String::from("machine example.com login foo password bar");
93    /// let netrc = Netrc::parse(input, false).unwrap();
94    /// assert_eq!(netrc.machines, vec![ Machine {
95    /// name: Some("example.com".to_string()),
96    /// account: None,
97    /// login: Some("foo".to_string()),
98    /// password: Some("bar".to_string()),
99    /// }]);
100    /// assert_eq!(netrc.machines[0].to_string(), "machine example.com login foo password bar".to_string());
101    /// ```
102    pub fn parse<T: AsRef<str>>(buf: T, unknown_entries: bool) -> Result<Netrc> {
103        Self::parse_borrow(&buf, unknown_entries)
104    }
105
106    /// Parse a `Netrc` format str with borrowing.
107    ///
108    /// If pass true to `unknown_entries`, it will collect unknown entries.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use netrc_rs::{Netrc, Machine};
114    ///
115    /// let input = String::from("machine 例子.com login foo password bar");
116    /// let netrc = Netrc::parse_borrow(&input, false).unwrap();
117    /// assert_eq!(netrc.machines, vec![Machine {
118    /// name: Some("例子.com".to_string()),
119    /// account: None,
120    /// login: Some("foo".to_string()),
121    /// password: Some("bar".to_string()),
122    /// }]);
123    /// assert_eq!(netrc.machines[0].to_string(), "machine 例子.com login foo password bar".to_string());
124    /// ```
125    pub fn parse_borrow<T: AsRef<str>>(buf: &T, unknown_entries: bool) -> Result<Netrc> {
126        let mut netrc = Netrc::default();
127        let mut lexer = Lexer::new::<T>(buf);
128        let mut count = MachineCount::default();
129        loop {
130            match lexer.next_token() {
131                Err(Error::EOF) => break,
132                Err(err) => return Err(err),
133                Ok(tok) => {
134                    netrc.parse_entry::<T>(&mut lexer, &tok, &mut count, unknown_entries)?;
135                }
136            }
137        }
138        Ok(netrc)
139    }
140
141    fn parse_entry<T: AsRef<str>>(
142        &mut self,
143        lexer: &mut Lexer,
144        item: &Token,
145        count: &mut MachineCount,
146        unknown_entries: bool,
147    ) -> Result<()> {
148        match item {
149            Token::Machine => {
150                let host_name = lexer.next_token()?;
151                self.machines.push(Default::default());
152                self.machines[count.machine].name = Some(host_name.to_string());
153                count.machine += 1;
154                Ok(())
155            }
156
157            Token::Default => {
158                self.machines.push(Default::default());
159                count.machine += 1;
160                Ok(())
161            }
162
163            Token::Login => {
164                let name = lexer.next_token()?.to_string();
165                count.login += 1;
166                if count.login > count.machine {
167                    return Err(Error::IllegalFormat(
168                        lexer.tokens.position(),
169                        "login must follow machine".to_string(),
170                    ));
171                } else {
172                    let last = self.machines.len() - 1;
173                    self.machines[last].login = Some(name)
174                }
175                Ok(())
176            }
177
178            Token::Password => {
179                let name = lexer.next_token()?.to_string();
180                count.password += 1;
181                if count.password > count.machine {
182                    return Err(Error::IllegalFormat(
183                        lexer.tokens.position(),
184                        "password must follow machine".to_string(),
185                    ));
186                } else {
187                    let last = self.machines.len() - 1;
188                    self.machines[last].password = Some(name)
189                }
190                Ok(())
191            }
192
193            Token::Account => {
194                let name = lexer.next_token()?.to_string();
195                count.account += 1;
196                if count.account > count.machine {
197                    return Err(Error::IllegalFormat(
198                        lexer.tokens.position(),
199                        "account must follow machine".to_string(),
200                    ));
201                } else {
202                    let last = self.machines.len() - 1;
203                    self.machines[last].account = Some(name)
204                }
205                Ok(())
206            }
207
208            // Just skip to end of macdefs
209            Token::MacDef => {
210                let name = lexer.next_token()?.to_string();
211                let cmds = lexer.next_commands();
212
213                self.macdefs.push((name, cmds));
214                Ok(())
215            }
216
217            Token::Str(s) if unknown_entries => {
218                self.unknown_entries.push(s.to_string());
219                Ok(())
220            }
221
222            Token::Str(s) => Err(Error::IllegalFormat(
223                lexer.tokens.position(),
224                "token: ".to_string() + s,
225            )),
226        }
227    }
228}
229
230#[derive(Debug, Default)]
231struct MachineCount {
232    machine: usize,
233    login: usize,
234    password: usize,
235    account: usize,
236}
237
238#[derive(Debug)]
239enum Token {
240    Machine,
241    Default,
242    Login,
243    Password,
244    Account,
245    MacDef,
246    Str(String),
247}
248
249impl Display for Token {
250    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
251        use Token::*;
252
253        let s = match self {
254            Machine => "machine",
255            Default => "default",
256            Login => "login",
257            Password => "password",
258            Account => "account",
259            MacDef => "macdef",
260            Str(s) => s,
261        };
262        write!(f, "{}", s)
263    }
264}
265
266impl Token {
267    fn new(s: String) -> Self {
268        use Token::*;
269
270        match s.as_str() {
271            "machine" => Machine,
272            "default" => Default,
273            "login" => Login,
274            "password" => Password,
275            "account" => Account,
276            "macdef" => MacDef,
277
278            _ => Str(s),
279        }
280    }
281}
282
283struct Tokens<'a> {
284    buf: Chars<'a>,
285    pos: Position,
286}
287
288impl Display for Position {
289    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
290        write!(f, "{}:{}", self.0, self.1)
291    }
292}
293
294impl<'a> Tokens<'a> {
295    fn new<T: AsRef<str>>(buf: &'a T) -> Self {
296        Self {
297            buf: buf.as_ref().chars(),
298            pos: Position(1, 1),
299        }
300    }
301
302    fn update_position(&mut self, ch: char) {
303        if ch == '\n' {
304            self.pos.0 += 1;
305            self.pos.1 = 1;
306        } else {
307            self.pos.1 += 1;
308        }
309    }
310
311    fn position(&self) -> Position {
312        self.pos
313    }
314
315    fn skip_whitespace(&mut self) {
316        for ch in self.buf.clone() {
317            if !ch.is_whitespace() {
318                break;
319            }
320
321            self.update_position(ch);
322            self.buf.next();
323        }
324    }
325
326    fn next_token(&mut self) -> Option<Token> {
327        self.skip_whitespace();
328        if self.buf.clone().next().is_some() {
329            let mut s = String::new();
330            for ch in self.buf.clone() {
331                if ch.is_whitespace() {
332                    break;
333                }
334
335                self.update_position(ch);
336                self.buf.next();
337                s.push(ch);
338            }
339
340            if s.is_empty() {
341                None
342            } else {
343                Some(Token::new(s))
344            }
345        } else {
346            None
347        }
348    }
349
350    fn next_commands(&mut self) -> Vec<String> {
351        self.skip_whitespace();
352        let mut cmds = Vec::new();
353        for line in self.buf.clone().as_str().lines() {
354            for _ in 0..=line.len() {
355                self.buf.next();
356            }
357            if line.is_empty() || line == "\n" {
358                break;
359            }
360            cmds.push(line.trim().to_string());
361        }
362        self.pos.0 += cmds.len();
363        self.pos.1 = 1;
364        cmds
365    }
366}
367
368struct Lexer<'a> {
369    tokens: Tokens<'a>,
370}
371
372impl<'a> Lexer<'a> {
373    fn new<T: AsRef<str>>(buf: &'a T) -> Self {
374        Self {
375            tokens: Tokens::new(buf),
376        }
377    }
378
379    fn next_token(&mut self) -> Result<Token> {
380        self.tokens.next_token().ok_or(Error::EOF)
381    }
382
383    fn next_commands(&mut self) -> Vec<String> {
384        self.tokens.next_commands()
385    }
386}
387
388#[cfg(test)]
389mod test {
390    use super::*;
391
392    #[test]
393    fn test_token() {
394        let input = r#"
395machine host1.com login login1
396macdef test
397cd /pub/tests
398bin
399put filename.tar.gz
400quit
401machine host2.com login login2"#
402            .to_string();
403        let mut tokens = Tokens::new(&input);
404        let strs: Vec<&str> = input.split_whitespace().collect();
405        let mut count = 0;
406        loop {
407            match tokens.next_token() {
408                Some(tok) => {
409                    assert_eq!(tok.to_string().as_str(), strs[count]);
410                    count += 1;
411                }
412                None => break,
413            }
414        }
415    }
416
417    #[test]
418    fn parse_simple() {
419        let input = "machine 中文.com login test password p@ssw0rd".to_string();
420        let netrc = Netrc::parse(input, false).unwrap();
421        assert_eq!(netrc.machines.len(), 1);
422        assert!(netrc.macdefs.is_empty());
423        let machine = netrc.machines[0].clone();
424        assert_eq!(machine.name, Some("中文.com".into()));
425        assert_eq!(machine.login, Some("test".into()));
426        assert_eq!(machine.password.as_ref().unwrap(), "p@ssw0rd");
427        assert_eq!(machine.account, None);
428    }
429
430    #[test]
431    fn parse_unknown() {
432        let input = "machine example.com login test my_entry1 password foo my_entry2".to_string();
433        let netrc = Netrc::parse(input, true).unwrap();
434        assert_eq!(netrc.machines.len(), 1);
435        assert!(netrc.macdefs.is_empty());
436        assert_eq!(
437            netrc.unknown_entries,
438            vec!["my_entry1".to_string(), "my_entry2".to_string()]
439        );
440
441        let machine = netrc.machines[0].clone();
442        assert_eq!(machine.name, Some("example.com".into()));
443        assert_eq!(machine.login, Some("test".into()));
444        assert_eq!(machine.password.as_ref().unwrap(), "foo");
445    }
446
447    #[test]
448    fn parse_macdef() {
449        let input = r#"machine host0.com login login0
450                     macdef uploadtest
451                            cd /pub/tests
452                            bin
453                            put filename.tar.gz
454                            echo 中文测试
455                            quit
456
457                     machine host1.com login login1"#;
458        let netrc = Netrc::parse(input, false).unwrap();
459        assert_eq!(netrc.machines.len(), 2);
460        for (i, machine) in netrc.machines.iter().enumerate() {
461            assert_eq!(machine.name, Some(format!("host{}.com", i)));
462            assert_eq!(machine.login, Some(format!("login{}", i)));
463        }
464        assert_eq!(netrc.macdefs.len(), 1);
465        let (ref name, ref cmds) = netrc.macdefs[0];
466        assert_eq!(name, "uploadtest");
467        assert_eq!(
468            *cmds,
469            vec![
470                "cd /pub/tests",
471                "bin",
472                "put filename.tar.gz",
473                "echo 中文测试",
474                "quit"
475            ]
476            .iter()
477            .map(|s| s.to_string())
478            .collect::<Vec<String>>()
479        )
480    }
481
482    #[test]
483    fn parse_default() {
484        let input = r#"machine example.com login test
485            default login def"#;
486        let netrc = Netrc::parse(input, false).unwrap();
487        assert_eq!(netrc.machines.len(), 2);
488
489        let machine = netrc.machines[0].clone();
490        assert_eq!(machine.name, Some("example.com".into()));
491        assert_eq!(machine.login, Some("test".into()));
492
493        let machine = netrc.machines[1].clone();
494        assert_eq!(machine.name, None);
495        assert_eq!(machine.login, Some("def".into()));
496    }
497
498    #[test]
499    fn parse_error_unknown_entry() {
500        let input = "machine foobar.com foo";
501        match Netrc::parse(input, false).unwrap_err() {
502            Error::IllegalFormat(_pos, _s) => {}
503            e => panic!("Error type: {:?}", e),
504        }
505    }
506
507    #[test]
508    fn parse_error_eof() {
509        let input = "machine foobar.com password melody login";
510        match Netrc::parse(input, false).unwrap_err() {
511            Error::EOF => {}
512            e => panic!("Error type: {}", e),
513        }
514    }
515
516    #[test]
517    fn parse_error_illegal_format() {
518        let input = "password bar login foo";
519        match Netrc::parse(input, false).unwrap_err() {
520            Error::IllegalFormat(_pos, _s) => {}
521            e => panic!("Error type: {}", e),
522        }
523    }
524}