1#![feature(closure_lifetime_binder)]
2
3mod error;
30pub use error::NetrcError;
31use log::{debug, error, info, warn};
32use nom::{IResult, Parser,
33 branch::alt,
34 bytes::complete::{tag, take_while1},
35 character::complete::{line_ending, multispace0, multispace1, not_line_ending},
36 combinator::{all_consuming, eof, opt},
37 multi::many0};
38use serde::{Deserialize, Serialize};
39use std::{collections::HashMap, fs, path::Path};
40
41#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
46pub struct NetrcMachine {
47 pub machine: String,
48 pub login: String,
49 pub password: String,
50 pub account: Option<String>,
51 pub macdef: Option<String>,
52}
53
54#[derive(Debug, PartialEq, Serialize, Deserialize)]
55pub struct Credentials {
56 pub email: String,
57 pub token: String,
58}
59
60#[derive(Debug, Default, Deserialize, Serialize)]
66pub struct Netrc {
67 pub machines: HashMap<String, NetrcMachine>,
68}
69
70fn is_token_char(c: char) -> bool {
74 !c.is_whitespace()
75}
76
77fn parse_token(input: &str) -> IResult<&str, &str> {
82 take_while1(is_token_char)(input)
83}
84
85fn parse_machine(input: &str) -> IResult<&str, NetrcMachine> {
92 debug!("Parsing machine entry from input: {:?}", input);
93 let (input, _) = multispace0(input)?;
94 let (input, key): (&str, &str) = alt((tag("machine"), tag("default"))).parse(input)?;
95 debug!("Parsed key: {}", key);
96 let (input, _) = multispace1(input)?;
97 let (input, machine_name) = if key == "default" {
98 (input, "default")
99 } else {
100 let (input, name) = parse_token(input).map_err(|_| {
101 debug!("Failed to parse machine name");
102 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify))
103 })?;
104 if name.trim().is_empty()
105 || name == "login"
106 || name == "password"
107 || name == "account"
108 || name == "macdef"
109 {
110 debug!("Invalid machine name: {}", name);
111 return Err(nom::Err::Error(nom::error::Error::new(
112 input,
113 nom::error::ErrorKind::Verify,
114 )));
115 }
116 (input, name)
117 };
118 debug!("Parsed machine name: {}", machine_name);
119
120 let mut login = String::new();
121 let mut password = String::new();
122 let mut account = None;
123 let mut macdef = None;
124 let mut rest = input;
125
126 loop {
127 let (next_input, _) = multispace0(rest)?;
128 if let Ok((next_input, _)) = eof::<_, nom::error::Error<_>>(next_input) {
129 debug!("Reached end of input for machine: {}", machine_name);
130 rest = next_input;
131 break;
132 }
133
134 let (next_input, token): (&str, Option<&str>) = opt(parse_token).parse(next_input)?;
135 match token {
136 Some("machine") | Some("default") => {
137 debug!(
138 "Encountered new machine or default, stopping parsing for: {}",
139 machine_name
140 );
141 break;
142 },
143 Some("login") => {
144 let (next_input, _) = multispace1(next_input)?;
145 let (next_input, val) = parse_token(next_input).map_err(|_| {
146 debug!("Failed to parse login token for machine: {}", machine_name);
147 nom::Err::Failure(nom::error::Error::new(
148 next_input,
149 nom::error::ErrorKind::Verify,
150 ))
151 })?;
152 login = val.to_string();
153 debug!("Parsed login: {} for machine: {}", login, machine_name);
154 rest = next_input;
155 },
156 Some("password") => {
157 let (next_input, _) = multispace1(next_input)?;
158 let (next_input, val) = parse_token(next_input).map_err(|_| {
159 debug!("Failed to parse password token for machine: {}", machine_name);
160 nom::Err::Error(nom::error::Error::new(
161 next_input,
162 nom::error::ErrorKind::Verify,
163 ))
164 })?;
165 password = val.to_string();
166 debug!("Parsed password for machine: {}", machine_name);
167 rest = next_input;
168 },
169 Some("account") => {
170 let (next_input, _) = multispace1(next_input)?;
171 let (next_input, val) = parse_token(next_input).map_err(|_| {
172 debug!("Failed to parse account token for machine: {}", machine_name);
173 nom::Err::Error(nom::error::Error::new(
174 next_input,
175 nom::error::ErrorKind::Verify,
176 ))
177 })?;
178 account = Some(val.to_string());
179 debug!("Parsed account: {} for machine: {}", val, machine_name);
180 rest = next_input;
181 },
182 Some("macdef") => {
183 let (next_input, _) = multispace1(next_input)?;
184 let (next_input, macdef_name) = parse_token(next_input).map_err(|_| {
185 debug!("Failed to parse macdef name for machine: {}", machine_name);
186 nom::Err::Error(nom::error::Error::new(
187 next_input,
188 nom::error::ErrorKind::Verify,
189 ))
190 })?;
191 debug!("Parsing macdef: {} for machine: {}", macdef_name, machine_name);
192 let (next_input, _) = line_ending(next_input)?;
193
194 let mut lines = Vec::new();
195 let mut current_input = next_input;
196 loop {
197 let (next, _) = multispace0(current_input)?;
198 let (next, line) = not_line_ending(next)?;
199 let (next, line_end) = opt(line_ending).parse(next)?;
200 if line.trim().is_empty() && line_end.is_some() {
201 debug!("Reached empty line, ending macdef for machine: {}", machine_name);
202 current_input = next;
203 break;
204 }
205 if next.is_empty() {
206 debug!("Reached end of macdef for machine: {}", machine_name);
207 current_input = next;
208 break;
209 }
210 lines.push(line.to_string());
211 current_input = next;
212 }
213
214 let macdef_content = lines.join("\n").trim_end().to_string();
215 macdef =
216 Some(if macdef_content.is_empty() { "" } else { &macdef_content }.to_string());
217 debug!("Parsed macdef content: {:?} for machine: {}", macdef, machine_name);
218 rest = current_input;
219 },
220 Some(token) => {
221 warn!("Unexpected token: {} for machine: {}, skipping", token, machine_name);
222 let (next_input, _) = multispace0(next_input)?;
223 rest = next_input;
224 },
225 None => {
226 debug!("No more tokens for machine: {}, stopping parsing", machine_name);
227 rest = next_input;
228 break;
229 },
230 }
231 }
232
233 if login.is_empty() {
234 warn!("No login provided for machine: {}", machine_name);
235 }
236 if password.is_empty() {
237 warn!("No password provided for machine: {}", machine_name);
238 }
239
240 Ok((rest, NetrcMachine { machine: machine_name.to_string(), login, password, account, macdef }))
241}
242
243fn parse_netrc(input: &str) -> IResult<&str, Netrc> {
248 info!("Parsing entire .netrc content");
249 let (input, machine_list) = all_consuming(many0(parse_machine)).parse(input)?;
250 debug!("Parsed {} machine entries", machine_list.len());
251 let mut machines = HashMap::new();
252 for machine in machine_list {
253 if machines.contains_key(&machine.machine) {
254 error!("Duplicate machine entry found: {}", machine.machine);
255 return Err(nom::Err::Failure(nom::error::Error::new(
256 input,
257 nom::error::ErrorKind::Many1,
258 )));
259 }
260 debug!("Adding machine entry: {}", machine.machine);
261 machines.insert(machine.machine.clone(), machine);
262 }
263 info!("Successfully parsed .netrc with {} machines", machines.len());
264 Ok((input, Netrc { machines }))
265}
266
267impl Netrc {
268 pub fn parse_from_str(input: &str) -> Result<Self, NetrcError> {
284 info!("Parsing .netrc string");
285 match parse_netrc(input) {
286 Ok((_, parsed)) => {
287 info!("Successfully parsed .netrc string");
288 Ok(parsed)
289 },
290 Err(e) => {
291 let err = match e {
292 nom::Err::Incomplete(_) => {
293 NetrcError::Parse {
294 message: "incomplete input".to_string(),
295 input: input.to_string(),
296 }
297 },
298 nom::Err::Error(e) => {
299 NetrcError::Parse {
300 message: format!("parse error: {:?}", e),
301 input: input.to_string(),
302 }
303 },
304 nom::Err::Failure(e) if e.code == nom::error::ErrorKind::Many1 => {
305 NetrcError::DuplicateEntry("duplicate machine entry".to_string())
306 },
307 nom::Err::Failure(e) => {
308 NetrcError::Parse {
309 message: format!("parse failure: {:?}", e),
310 input: input.to_string(),
311 }
312 },
313 };
314 error!("Failed to parse .netrc string: {}", err);
315 Err(err)
316 },
317 }
318 }
319
320 pub fn parse_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NetrcError> {
352 let path = path.as_ref();
353 info!("Reading and parsing .netrc file from path: {:?}", path);
354 let metadata = fs::metadata(path).map_err(|e| {
355 error!("Failed to read metadata for {:?}: {}", path, e);
356 if e.kind() == std::io::ErrorKind::NotFound {
357 NetrcError::FileNotFound(path.display().to_string())
358 } else {
359 NetrcError::Io(e)
360 }
361 })?;
362 #[cfg(unix)]
363 {
364 use std::os::unix::fs::PermissionsExt;
365 let mode = metadata.permissions().mode();
366 if mode & 0o077 != 0 {
367 error!("File permissions for {:?} are too open: {:o}", path, mode);
368 return Err(NetrcError::InsecurePermissions);
369 }
370 }
371 match fs::read_to_string(path) {
372 Ok(content) => {
373 debug!("Successfully read .netrc file from {:?}", path);
374 Self::parse_from_str(&content)
375 },
376 Err(e) => {
377 error!("Failed to read .netrc file from {:?}: Error: {}", path, e);
378 if e.kind() == std::io::ErrorKind::NotFound {
379 Err(NetrcError::FileNotFound(path.display().to_string()))
380 } else {
381 Err(NetrcError::Io(e))
382 }
383 },
384 }
385 }
386
387 pub fn get_credentials(&self, machine: &str) -> Option<Credentials> {
388 self.get(machine).map(|m| Credentials { email: m.login.clone(), token: m.password.clone() })
389 }
390
391 pub fn get(&self, machine: &str) -> Option<&NetrcMachine> {
395 debug!("Retrieving machine entry for: {}", machine);
396 let result = self.machines.get(machine);
397 if result.is_none() {
398 warn!("No machine entry found for: {}", machine);
399 }
400 result
401 }
402
403 pub fn to_json(&self) -> Result<String, NetrcError> {
408 info!("Serializing .netrc to JSON");
409 match serde_json::to_string_pretty(self) {
410 Ok(json) => {
411 debug!("Successfully serialized .netrc to JSON");
412 Ok(json)
413 },
414 Err(e) => {
415 error!("Failed to serialize .netrc to JSON: {}", e);
416 Err(NetrcError::Serialize(e.to_string()))
417 },
418 }
419 }
420
421 #[cfg(feature = "toml")]
426 pub fn to_toml(&self) -> Result<String, NetrcError> {
427 info!("Serializing .netrc to TOML");
428 match toml::to_string_pretty(self) {
429 Ok(toml) => {
430 debug!("Successfully serialized .netrc to TOML");
431 Ok(toml)
432 },
433 Err(e) => {
434 error!("Failed to serialize .netrc to TOML: {}", e);
435 Err(NetrcError::Serialize(e.to_string()))
436 },
437 }
438 }
439
440 pub fn insert_machine(&mut self, machine: NetrcMachine) {
444 info!("Inserting or replacing machine entry: {}", machine.machine);
445 self.machines.insert(machine.machine.clone(), machine);
446 debug!("Machine entry inserted: {}", self.machines.len());
447 }
448
449 pub fn remove_machine(&mut self, machine_name: &str) -> Option<NetrcMachine> {
454 info!("Removing machine entry: {}", machine_name);
455 let result = self.machines.remove(machine_name);
456 if result.is_some() {
457 debug!("Successfully removed machine entry: {}", machine_name);
458 } else {
459 warn!("No machine entry found to remove: {}", machine_name);
460 }
461 result
462 }
463
464 pub fn update_machine<F>(&mut self, machine_name: &str, update_fn: F) -> Result<(), NetrcError>
487 where
488 F: FnOnce(&mut NetrcMachine),
489 {
490 info!("Updating machine entry: {}", machine_name);
491 if let Some(machine) = self.machines.get_mut(machine_name) {
492 update_fn(machine);
493 debug!("Successfully updated machine entry: {}", machine_name);
494 Ok(())
495 } else {
496 error!("Failed to update machine entry: {} not found", machine_name);
497 Err(NetrcError::NotFound(machine_name.to_string()))
498 }
499 }
500
501 pub fn to_netrc_string(&self) -> String {
505 info!("Serializing .netrc to string format");
506 let mut output = String::new();
507 for machine in self.machines.values() {
508 debug!("Serializing machine entry: {}", machine.machine);
509 if machine.machine == "default" {
510 output.push_str("default\n");
511 } else {
512 output.push_str(&format!("machine {}\n", machine.machine));
513 }
514 output.push_str(&format!(" login {}\n", machine.login));
515 output.push_str(&format!(" password {}\n", machine.password));
516 if let Some(account) = &machine.account {
517 output.push_str(&format!(" account {}\n", account));
518 }
519 if let Some(macdef) = &machine.macdef {
520 output.push_str(&format!(" macdef init\n{}\n\n", macdef));
521 }
522 }
523 debug!("Completed serialization to .netrc string");
524 output
525 }
526
527 pub fn save_to_path<P: AsRef<Path>>(&self, path: P) -> Result<(), NetrcError> {
555 let path = path.as_ref();
556 info!("Saving .netrc to path: {:?}", path);
557 let netrc_string = self.to_netrc_string();
558 match fs::write(path, &netrc_string) {
559 Ok(()) => {
560 #[cfg(unix)]
561 {
562 use std::os::unix::fs::PermissionsExt;
563 fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|e| {
564 error!("Failed to set permissions for {:?}: {}", path, e);
565 NetrcError::Io(e)
566 })?;
567 }
568 debug!("Successfully saved .netrc to {:?}", path);
569 Ok(())
570 },
571 Err(e) => {
572 error!("Failed to save .netrc to {:?}: {}", path, e);
573 Err(NetrcError::Io(e))
574 },
575 }
576 }
577
578 pub fn set_credentials(
579 &mut self,
580 machine: &str,
581 login: &str,
582 password: &str,
583 ) -> Result<(), NetrcError> {
584 let machine_entry = NetrcMachine {
585 machine: machine.to_string(),
586 login: login.to_string(),
587 password: password.to_string(),
588 account: None,
589 macdef: None,
590 };
591 self.insert_machine(machine_entry);
592 Ok(())
593 }
594
595 pub fn remove_credentials(&mut self, machine: &str) -> Option<NetrcMachine> {
596 self.remove_machine(machine)
597 }
598
599 pub fn merge(&mut self, other: Netrc) {
600 for (name, machine) in other.machines {
601 self.machines.insert(name, machine);
602 }
603 }
604
605 }
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use log::Level;
621 use std::cell::RefCell;
622
623 thread_local! {
624 static LOG_MESSAGES: RefCell<Vec<(Level, String)>> = const { RefCell::new(Vec::new()) };
625 }
626
627 struct TestLogger;
629
630 impl log::Log for TestLogger {
631 fn enabled(&self, metadata: &log::Metadata) -> bool {
632 metadata.level() <= Level::Debug
633 }
634
635 fn log(&self, record: &log::Record) {
636 if self.enabled(record.metadata()) {
637 eprintln!("Log: {} - {}", record.level(), record.args());
638 LOG_MESSAGES.with(|messages| {
639 messages.borrow_mut().push((record.level(), format!("{}", record.args())));
640 });
641 }
642 }
643
644 fn flush(&self) {
645 }
646 }
647
648 fn init_logger() {
649 eprintln!("Initializing logger with RUST_LOG={:?}", std::env::var("RUST_LOG"));
650 let _ = log::set_logger(&TestLogger).map(|()| log::set_max_level(log::LevelFilter::Debug));
651 LOG_MESSAGES.with(|messages| messages.borrow_mut().clear());
652 }
653
654 fn get_log_messages() -> Vec<(Level, String)> {
656 LOG_MESSAGES.with(|messages| messages.borrow().clone())
657 }
658
659 #[test]
661 fn parse_basic_entry() {
662 init_logger();
663 let input = "machine example.com login user password pass";
664 let netrc = Netrc::parse_from_str(input).unwrap();
665 let creds = netrc.get("example.com").unwrap();
666
667 assert_eq!(creds.login, "user");
668 assert_eq!(creds.password, "pass");
669 assert!(creds.account.is_none());
670
671 let messages = get_log_messages();
672 assert!(messages.iter().any(|(level, msg)| {
673 *level == Level::Info && msg.contains("Parsing .netrc string")
674 }));
675 assert!(messages.iter().any(|(level, msg)| {
676 *level == Level::Debug && msg.contains("Parsed machine name: example.com")
677 }));
678 }
679
680 #[test]
682 fn parse_with_account() {
683 init_logger();
684 let input = "machine api.com login alice password secret account dev";
685 let netrc = Netrc::parse_from_str(input).unwrap();
686 let creds = netrc.get("api.com").unwrap();
687
688 assert_eq!(creds.account.as_deref(), Some("dev"));
689
690 let messages = get_log_messages();
691 assert!(
692 messages
693 .iter()
694 .any(|(level, msg)| *level == Level::Debug && msg.contains("Parsed account: dev"))
695 );
696 }
697
698 #[test]
700 fn parse_default_entry() {
701 init_logger();
702 let input = "default login guest password guess123";
703 let netrc = Netrc::parse_from_str(input).unwrap();
704 let creds = netrc.get("default").unwrap();
705
706 assert_eq!(creds.login, "guest");
707 assert_eq!(creds.password, "guess123");
708
709 let messages = get_log_messages();
710 assert!(
711 messages
712 .iter()
713 .any(|(level, msg)| *level == Level::Debug && msg.contains("Parsed key: default"))
714 );
715 }
716
717 #[test]
719 fn parse_macdef_and_account() {
720 init_logger();
721 let input = r#"
722 machine internal login root password rootpass account admin
723 macdef init
724 echo Initializing connection...
725
726 "#;
727
728 let netrc = Netrc::parse_from_str(input).unwrap();
729 let creds = netrc.get("internal").unwrap();
730
731 assert_eq!(creds.account.as_deref(), Some("admin"));
732 assert!(creds.macdef.is_some());
733 assert!(
734 creds
735 .macdef
736 .as_ref()
737 .map(|m| m.contains("echo Initializing connection"))
738 .unwrap_or(false)
739 );
740
741 let messages = get_log_messages();
742 assert!(messages.iter().any(|(level, msg)| {
743 *level == Level::Debug && msg.contains("Parsing macdef: init")
744 }));
745 }
746
747 #[test]
749 fn empty_input_returns_empty_netrc() {
750 init_logger();
751 let netrc = Netrc::parse_from_str("").unwrap();
752 assert!(netrc.machines.is_empty());
753
754 let messages = get_log_messages();
755 assert!(messages.iter().any(|(level, msg)| {
756 *level == Level::Info && msg.contains("Successfully parsed .netrc with 0 machines")
757 }));
758 }
759
760 #[test]
762 fn missing_login_password_fields() {
763 init_logger();
764 let input = "machine foo.com";
765 let netrc = Netrc::parse_from_str(input).unwrap();
766 let creds = netrc.get("foo.com").unwrap();
767 assert_eq!(creds.login, "");
768 assert_eq!(creds.password, "");
769
770 let messages = get_log_messages();
771 assert!(messages.iter().any(|(level, msg)| {
772 *level == Level::Warn && msg.contains("No login provided for machine: foo.com")
773 }));
774 assert!(messages.iter().any(|(level, msg)| {
775 *level == Level::Warn && msg.contains("No password provided for machine: foo.com")
776 }));
777 }
778
779 #[test]
780 fn parse_duplicate_machine_fails() {
781 init_logger();
782 let input = "machine example.com login user1 password pass1\nmachine example.com login user2 password pass2";
783 let result = Netrc::parse_from_str(input);
784 assert!(matches!(result, Err(NetrcError::DuplicateEntry(_))));
785 }
786
787 #[test]
788 fn parse_invalid_token_after_login() {
789 init_logger();
790 let input = "machine example.com login ";
791 let result = Netrc::parse_from_str(input);
792 assert!(matches!(result, Err(NetrcError::Parse { message: _, input: _ })));
793 }
794
795 #[test]
796 fn parse_multiple_machines() {
797 init_logger();
798 let input = "machine example.com login user1 password pass1\nmachine api.com login user2 password pass2";
799 let netrc = Netrc::parse_from_str(input).unwrap();
800 assert_eq!(netrc.machines.len(), 2);
801 assert!(netrc.get("example.com").is_some());
802 assert!(netrc.get("api.com").is_some());
803 }
804
805 #[test]
806 fn parse_whitespace_heavy_input() {
807 init_logger();
808 let input = "\t\n machine example.com \n\t login \t user \n password pass \n";
809 let netrc = Netrc::parse_from_str(input).unwrap();
810 let creds = netrc.get("example.com").unwrap();
811 assert_eq!(creds.login, "user");
812 assert_eq!(creds.password, "pass");
813
814 let messages = get_log_messages();
815 assert!(
816 messages
817 .iter()
818 .any(|(level, msg)| *level == Level::Debug && msg.contains("Parsed login: user"))
819 );
820 }
821
822 #[test]
823 fn parse_empty_macdef() {
824 init_logger();
825 let input = "machine example.com login user password pass macdef init\n\n";
826 let netrc = Netrc::parse_from_str(input).unwrap();
827 let creds = netrc.get("example.com").unwrap();
828 assert_eq!(creds.macdef, Some("".to_string()));
829 }
830
831 #[test]
832 fn insert_and_update_machine() {
833 init_logger();
834 let mut netrc = Netrc::default();
835 let machine = NetrcMachine {
836 machine: "example.com".to_string(),
837 login: "user".to_string(),
838 password: "pass".to_string(),
839 account: None,
840 macdef: None,
841 };
842 netrc.insert_machine(machine.clone());
843 assert_eq!(netrc.get("example.com").unwrap().login, "user");
844
845 netrc.update_machine("example.com", |m| m.login = "new_user".to_string()).unwrap();
846 assert_eq!(netrc.get("example.com").unwrap().login, "new_user");
847
848 let result = netrc.update_machine("nonexistent.com", |_| {});
849 assert!(matches!(result, Err(NetrcError::NotFound(_))));
850
851 let messages = get_log_messages();
852 assert!(messages.iter().any(|(level, msg)| {
853 *level == Level::Info
854 && msg.contains("Inserting or replacing machine entry: example.com")
855 }));
856 assert!(messages.iter().any(|(level, msg)| {
857 *level == Level::Info && msg.contains("Updating machine entry: example.com")
858 }));
859 assert!(messages.iter().any(|(level, msg)| {
860 *level == Level::Error
861 && msg.contains("Failed to update machine entry: nonexistent.com")
862 }));
863 }
864
865 #[test]
866 fn remove_machine() {
867 init_logger();
868 let mut netrc = Netrc::default();
869 let machine = NetrcMachine {
870 machine: "example.com".to_string(),
871 login: "user".to_string(),
872 password: "pass".to_string(),
873 account: None,
874 macdef: None,
875 };
876 netrc.insert_machine(machine.clone());
877 let removed = netrc.remove_machine("example.com").unwrap();
878 assert_eq!(removed, machine);
879 assert!(netrc.get("example.com").is_none());
880 assert!(netrc.remove_machine("example.com").is_none());
881
882 let messages = get_log_messages();
883 assert!(messages.iter().any(|(level, msg)| {
884 *level == Level::Info && msg.contains("Removing machine entry: example.com")
885 }));
886 assert!(messages.iter().any(|(level, msg)| {
887 *level == Level::Debug
888 && msg.contains("Successfully removed machine entry: example.com")
889 }));
890 assert!(messages.iter().any(|(level, msg)| {
891 *level == Level::Warn && msg.contains("No machine entry found to remove: example.com")
892 }));
893 }
894
895 #[test]
896 fn serialize_to_json_and_toml() {
897 init_logger();
898 let mut netrc = Netrc::default();
899 let machine = NetrcMachine {
900 machine: "example.com".to_string(),
901 login: "user".to_string(),
902 password: "pass".to_string(),
903 account: Some("dev".to_string()),
904 macdef: None,
905 };
906 netrc.insert_machine(machine);
907
908 let json = netrc.to_json().unwrap();
909 assert!(json.contains(r#""machine": "example.com""#));
910 assert!(json.contains(r#""login": "user""#));
911
912 let toml = netrc.to_toml().unwrap();
913 assert!(toml.contains("machine = \"example.com\""));
914 assert!(toml.contains("login = \"user\""));
915
916 let messages = get_log_messages();
917 assert!(messages.iter().any(|(level, msg)| {
918 *level == Level::Info && msg.contains("Serializing .netrc to JSON")
919 }));
920 assert!(messages.iter().any(|(level, msg)| {
921 *level == Level::Info && msg.contains("Serializing .netrc to TOML")
922 }));
923 }
924
925 #[test]
926 fn round_trip_serialization() {
927 init_logger();
928 let mut netrc = Netrc::default();
929 let machine = NetrcMachine {
930 machine: "example.com".to_string(),
931 login: "user".to_string(),
932 password: "pass".to_string(),
933 account: Some("dev".to_string()),
934 macdef: Some("echo test".to_string()),
935 };
936 netrc.insert_machine(machine.clone());
937
938 let netrc_string = netrc.to_netrc_string();
939 let parsed_netrc = Netrc::parse_from_str(&netrc_string).unwrap();
940 assert_eq!(parsed_netrc.get("example.com").unwrap(), &machine);
941
942 let messages = get_log_messages();
943 assert!(messages.iter().any(|(level, msg)| {
944 *level == Level::Info && msg.contains("Serializing .netrc to string format")
945 }));
946 assert!(messages.iter().any(|(level, msg)| {
947 *level == Level::Info && msg.contains("Parsing .netrc string")
948 }));
949 }
950
951 #[test]
952 fn file_io_round_trip() {
953 init_logger();
954 let temp_file = std::env::temp_dir().join("test_netrc");
955 let mut netrc = Netrc::default();
956 let machine = NetrcMachine {
957 machine: "example.com".to_string(),
958 login: "user".to_string(),
959 password: "pass".to_string(),
960 account: None,
961 macdef: None,
962 };
963 netrc.insert_machine(machine.clone());
964
965 netrc.save_to_path(&temp_file).unwrap();
966 let loaded_netrc = Netrc::parse_from_path(&temp_file).unwrap();
967 assert_eq!(loaded_netrc.get("example.com").unwrap(), &machine);
968
969 std::fs::remove_file(&temp_file).unwrap();
970
971 let messages = get_log_messages();
972 assert!(messages.iter().any(|(level, msg)| {
973 *level == Level::Info && msg.contains("Saving .netrc to path")
974 }));
975 assert!(messages.iter().any(|(level, msg)| {
976 *level == Level::Info && msg.contains("Reading and parsing .netrc file")
977 }));
978 }
979
980 #[test]
981 fn parse_invalid_file_path() {
982 init_logger();
983 let result = Netrc::parse_from_path("/nonexistent/path/netrc");
984 assert!(matches!(result, Err(NetrcError::FileNotFound(_))));
985 let messages = get_log_messages();
986 assert!(messages.iter().any(|(level, msg)| {
987 *level == Level::Error && msg.contains("Failed to read metadata for")
988 }));
989 }
990
991 #[test]
992 fn parse_complex_macdef() {
993 init_logger();
994 let input = r#"
995 machine example.com login user password pass
996 macdef init
997 echo Starting...
998 sleep 1
999 echo Done
1000
1001 "#;
1002 let netrc = Netrc::parse_from_str(input).unwrap();
1003 let creds = netrc.get("example.com").unwrap();
1004 assert!(creds.macdef.is_some());
1005 let macdef = creds.macdef.as_ref().unwrap();
1006 assert!(macdef.contains("echo Starting..."));
1007 assert!(macdef.contains("sleep 1"));
1008 assert!(macdef.contains("echo Done"));
1009
1010 let messages = get_log_messages();
1011 assert!(messages.iter().any(|(level, msg)| {
1012 *level == Level::Debug && msg.contains("Parsed macdef content")
1013 }));
1014 }
1015
1016 #[test]
1017 fn parse_empty_machine_name() {
1018 init_logger();
1019 let input = "machine login user password pass";
1020 let result = Netrc::parse_from_str(input);
1021 assert!(matches!(result, Err(NetrcError::Parse { message: _, input: _ })));
1022 }
1023
1024 #[test]
1025 fn test_logging() {
1026 init_logger();
1027 let input = "machine example.com login user password pass";
1028 let netrc = Netrc::parse_from_str(input).unwrap();
1029 netrc.to_json().unwrap();
1030 netrc.to_toml().unwrap();
1031 netrc.to_netrc_string();
1032 netrc.get("example.com").unwrap();
1033 netrc.get("nonexistent.com");
1034
1035 let temp_file = std::env::temp_dir().join("test_netrc_log");
1036 netrc.save_to_path(&temp_file).unwrap();
1037 let _ = Netrc::parse_from_path(&temp_file);
1038 std::fs::remove_file(&temp_file).unwrap();
1039
1040 let mut netrc = Netrc::default();
1041 let machine = NetrcMachine {
1042 machine: "test.com".to_string(),
1043 login: "test".to_string(),
1044 password: "test".to_string(),
1045 account: None,
1046 macdef: None,
1047 };
1048 netrc.insert_machine(machine.clone());
1049 netrc.update_machine("test.com", |m| m.login = "updated".to_string()).unwrap();
1050 netrc.remove_machine("test.com");
1051
1052 let messages = get_log_messages();
1053 assert!(messages.iter().any(|(level, msg)| {
1054 *level == Level::Info && msg.contains("Parsing .netrc string")
1055 }));
1056 assert!(messages.iter().any(|(level, msg)| {
1057 *level == Level::Info && msg.contains("Serializing .netrc to JSON")
1058 }));
1059 assert!(messages.iter().any(|(level, msg)| {
1060 *level == Level::Info && msg.contains("Serializing .netrc to TOML")
1061 }));
1062 assert!(messages.iter().any(|(level, msg)| {
1063 *level == Level::Info && msg.contains("Serializing .netrc to string format")
1064 }));
1065 assert!(messages.iter().any(|(level, msg)| {
1066 *level == Level::Debug && msg.contains("Retrieving machine entry for: example.com")
1067 }));
1068 assert!(messages.iter().any(|(level, msg)| {
1069 *level == Level::Warn && msg.contains("No machine entry found for: nonexistent.com")
1070 }));
1071 assert!(messages.iter().any(|(level, msg)| {
1072 *level == Level::Info && msg.contains("Saving .netrc to path")
1073 }));
1074 assert!(messages.iter().any(|(level, msg)| {
1075 *level == Level::Info && msg.contains("Reading and parsing .netrc file")
1076 }));
1077 assert!(messages.iter().any(|(level, msg)| {
1078 *level == Level::Info && msg.contains("Inserting or replacing machine entry: test.com")
1079 }));
1080 assert!(messages.iter().any(|(level, msg)| {
1081 *level == Level::Info && msg.contains("Updating machine entry: test.com")
1082 }));
1083 assert!(messages.iter().any(|(level, msg)| {
1084 *level == Level::Info && msg.contains("Removing machine entry: test.com")
1085 }));
1086 }
1087
1088 #[test]
1089 fn parse_macdef_with_trailing_whitespace() {
1090 init_logger();
1091 let input = "machine example.com login user password pass macdef init\n \n";
1092 let netrc = Netrc::parse_from_str(input).unwrap();
1093 let creds = netrc.get("example.com").unwrap();
1094 assert_eq!(creds.macdef, Some("".to_string()));
1095 }
1096
1097 #[test]
1098 fn parse_macdef_with_multiple_empty_lines() {
1099 init_logger();
1100 let input = "machine example.com login user password pass macdef init\n\n\n";
1101 let netrc = Netrc::parse_from_str(input).unwrap();
1102 let creds = netrc.get("example.com").unwrap();
1103 assert_eq!(creds.macdef, Some("".to_string()));
1104 }
1105
1106 #[cfg(unix)]
1107 #[test]
1108 fn parse_file_with_insecure_permissions() {
1109 init_logger();
1110 let temp_file = std::env::temp_dir().join("test_netrc_perm");
1111 fs::write(&temp_file, "machine example.com login user password pass").unwrap();
1112 use std::os::unix::fs::PermissionsExt;
1113 fs::set_permissions(&temp_file, fs::Permissions::from_mode(0o666)).unwrap();
1114 let result = Netrc::parse_from_path(&temp_file);
1115 assert!(matches!(result, Err(NetrcError::InsecurePermissions)));
1116 let messages = get_log_messages();
1117 assert!(messages.iter().any(|(level, msg)| {
1118 *level == Level::Error && msg.contains("File permissions for")
1119 }));
1120 std::fs::remove_file(&temp_file).unwrap();
1121 }
1122}