1pub use netrc::{Authenticator, Netrc};
31use std::fs;
32use std::io;
33use std::io::ErrorKind;
34#[cfg(windows)]
35use std::iter::repeat;
36use std::path::{Path, PathBuf};
37use std::result;
38
39mod lex;
40mod netrc;
41
42pub type Result<T> = result::Result<T, Error>;
43
44#[derive(thiserror::Error, Debug)]
46pub enum Error {
47 #[error("I/O error: {0}")]
49 Io(#[from] std::io::Error),
50
51 #[error("{parser} in the file '{filename}'")]
53 Parsing {
54 parser: netrc::ParsingError,
55 filename: String,
56 },
57}
58
59impl Netrc {
60 pub fn new() -> Result<Self> {
65 Self::get_file()
66 .ok_or(Error::Io(io::Error::new(
67 ErrorKind::NotFound,
68 "no netrc file found",
69 )))
70 .and_then(|f| Self::from_file(f.as_path()))
71 }
72
73 #[expect(
75 clippy::disallowed_methods,
76 reason = "Preserve the upstream I/O error surface."
77 )]
78 pub fn from_file(file: &Path) -> Result<Self> {
79 String::from_utf8_lossy(&fs::read(file)?)
80 .parse()
81 .map_err(|e| Error::Parsing {
82 parser: e,
83 filename: file.display().to_string(),
84 })
85 }
86
87 fn get_file() -> Option<PathBuf> {
92 let env_var = std::env::var("NETRC")
93 .map(PathBuf::from)
94 .map(|f| shellexpand::path::tilde(&f).into_owned());
95
96 #[cfg(windows)]
97 let default = std::env::var("USERPROFILE")
98 .into_iter()
99 .flat_map(|home| repeat(home).zip([".netrc", "_netrc"]))
100 .map(|(home, file)| PathBuf::from(home).join(file));
101
102 #[cfg(not(windows))]
103 let default = std::env::var("HOME").map(|home| PathBuf::from(home).join(".netrc"));
104
105 env_var.into_iter().chain(default).find(|f| f.exists())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::sync::atomic::{AtomicUsize, Ordering};
113
114 static NETRC_COUNTER: AtomicUsize = AtomicUsize::new(0);
115
116 const CONTENT: &str = "\
117machine cocolog-nifty.com
118login jmarten0
119password cC2&yt7ZX
120
121machine wired.com
122login mstanlack1
123password gH4={wx=>VixU
124
125machine joomla.org
126login mbutterley2
127password hY5>yKqU&$vq&0
128";
129
130 fn create_netrc_file() -> PathBuf {
131 let id = NETRC_COUNTER.fetch_add(1, Ordering::Relaxed);
132 let dest = std::env::temp_dir().join(format!("mynetrc-{}-{id}", std::process::id()));
133 fs_err::write(&dest, CONTENT).unwrap();
134 dest
135 }
136
137 fn check_nrc(nrc: &Netrc) {
138 assert_eq!(nrc.hosts.len(), 3);
139 assert_eq!(
140 nrc.hosts["cocolog-nifty.com"],
141 Authenticator::new("jmarten0", "", "cC2&yt7ZX")
142 );
143 assert_eq!(
144 nrc.hosts["wired.com"],
145 Authenticator::new("mstanlack1", "", "gH4={wx=>VixU")
146 );
147 assert_eq!(
148 nrc.hosts["joomla.org"],
149 Authenticator::new("mbutterley2", "", "hY5>yKqU&$vq&0")
150 );
151 }
152
153 #[test]
154 fn test_new_env() {
155 let fi = create_netrc_file();
156 temp_env::with_var("NETRC", Some(fi.as_os_str()), || {
157 let nrc = Netrc::new().unwrap();
158 check_nrc(&nrc);
159 });
160 }
161
162 #[test]
163 fn test_from_file_failed() {
164 let err = Netrc::from_file(Path::new("/netrc/file/not/exists/on/no/netrc")).unwrap_err();
165 assert!(
166 matches!(&err, Error::Io(err) if err.kind() == ErrorKind::NotFound),
167 "expected NotFound I/O error, got {err}",
168 );
169 }
170
171 #[test]
172 fn test_from_file() {
173 let fi = create_netrc_file();
174 let nrc = Netrc::from_file(fi.as_path()).unwrap();
175 check_nrc(&nrc);
176 }
177}