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