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