aria2_core/config/
netrc.rs1use std::collections::HashMap;
2use std::fmt;
3use std::path::Path;
4
5#[derive(Debug, Clone)]
6pub struct NetRcEntry {
7 pub machine: String,
8 pub login: Option<String>,
9 pub password: Option<String>,
10 pub account: Option<String>,
11 pub macdef: Option<String>,
12}
13
14impl NetRcEntry {
15 pub fn new(machine: impl Into<String>) -> Self {
16 Self {
17 machine: machine.into(),
18 login: None,
19 password: None,
20 account: None,
21 macdef: None,
22 }
23 }
24
25 pub fn has_credentials(&self) -> bool {
26 self.login.is_some() && self.password.is_some()
27 }
28}
29
30#[derive(Debug, Clone)]
31pub struct NetRcFile {
32 entries: Vec<NetRcEntry>,
33 default_entry: Option<NetRcEntry>,
34 path: Option<String>,
35}
36
37#[derive(Debug, Clone)]
38pub enum NetRcError {
39 FileNotFound(String),
40 ParseError(String),
41 IoError(String),
42}
43
44impl fmt::Display for NetRcError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::FileNotFound(p) => write!(f, "netrc file not found: {}", p),
48 Self::ParseError(e) => write!(f, "netrc parse error: {}", e),
49 Self::IoError(e) => write!(f, "netrc io error: {}", e),
50 }
51 }
52}
53
54impl std::error::Error for NetRcError {}
55
56impl NetRcFile {
57 pub fn new() -> Self {
58 Self {
59 entries: Vec::new(),
60 default_entry: None,
61 path: None,
62 }
63 }
64
65 pub fn from_file(path: &str) -> Result<Self, NetRcError> {
66 let p = Path::new(path);
67 if !p.exists() {
68 return Err(NetRcError::FileNotFound(path.to_string()));
69 }
70 let content = std::fs::read_to_string(path)
71 .map_err(|e| NetRcError::IoError(format!("{}: {}", path, e)))?;
72 let mut parser = Self::new();
73 parser.path = Some(path.to_string());
74 parser.parse(&content)?;
75 Ok(parser)
76 }
77
78 pub fn parse(&mut self, content: &str) -> Result<(), NetRcError> {
79 let mut current_entry: Option<NetRcEntry> = None;
80 let mut in_macdef = false;
81 let mut macdef_name: Option<String> = None;
82 let mut macdef_lines: Vec<String> = Vec::new();
83
84 for line in content.lines() {
85 let line = line.trim();
86 if line.is_empty() || line.starts_with('#') {
87 continue;
88 }
89
90 if in_macdef {
91 if line.is_empty() {
92 in_macdef = false;
93 if let (Some(entry), Some(_name)) = (&mut current_entry, macdef_name.take()) {
94 entry.macdef = Some(macdef_lines.join("\n"));
95 macdef_lines.clear();
96 }
97 } else {
98 macdef_lines.push(line.to_string());
99 }
100 continue;
101 }
102
103 let tokens: Vec<&str> = line.split_whitespace().collect();
104 if tokens.is_empty() {
105 continue;
106 }
107
108 match tokens[0].to_lowercase().as_str() {
109 "machine" => {
110 if let Some(prev) = current_entry.take() {
111 if prev.machine == "default" {
112 self.default_entry = Some(prev);
113 } else {
114 self.entries.push(prev);
115 }
116 }
117 let machine_name = if tokens.len() > 1 {
118 tokens[1].to_string()
119 } else {
120 String::new()
121 };
122 current_entry = Some(NetRcEntry::new(machine_name));
123 }
124 "default" => {
125 if let Some(prev) = current_entry.take() {
126 self.entries.push(prev);
127 }
128 current_entry = Some(NetRcEntry::new("default".to_string()));
129 }
130 "login" => {
131 if let Some(ref mut entry) = current_entry {
132 entry.login = if tokens.len() > 1 {
133 Some(tokens[1].to_string())
134 } else {
135 None
136 };
137 }
138 }
139 "password" | "passwd" => {
140 if let Some(ref mut entry) = current_entry {
141 entry.password = if tokens.len() > 1 {
142 Some(tokens[1].to_string())
143 } else {
144 None
145 };
146 }
147 }
148 "account" => {
149 if let Some(ref mut entry) = current_entry {
150 entry.account = if tokens.len() > 1 {
151 Some(tokens[1].to_string())
152 } else {
153 None
154 };
155 }
156 }
157 "macdef" => {
158 macdef_name = if tokens.len() > 1 {
159 Some(tokens[1].to_string())
160 } else {
161 None
162 };
163 in_macdef = true;
164 macdef_lines.clear();
165 }
166 _ => {}
167 }
168 }
169
170 if let Some(entry) = current_entry.take() {
171 if entry.machine == "default" {
172 self.default_entry = Some(entry);
173 } else {
174 self.entries.push(entry);
175 }
176 }
177
178 Ok(())
179 }
180
181 pub fn find(&self, host: &str) -> Option<&NetRcEntry> {
182 for entry in &self.entries {
183 if entry.machine == host {
184 return Some(entry);
185 }
186 }
187 if host.contains('.') {
188 let domain_parts: Vec<&str> = host.split('.').collect();
189 if domain_parts.len() >= 2 {
190 let domain = domain_parts[domain_parts.len() - 2..].join(".");
191 for entry in &self.entries {
192 if entry.machine == domain {
193 return Some(entry);
194 }
195 }
196 }
197 }
198 None
199 }
200
201 pub fn find_default(&self) -> Option<&NetRcEntry> {
202 self.default_entry.as_ref()
203 }
204
205 pub fn get_credentials(&self, host: &str) -> Option<(String, String)> {
206 if let Some(entry) = self.find(host)
207 && let (Some(login), Some(pass)) = (&entry.login, &entry.password)
208 {
209 return Some((login.clone(), pass.clone()));
210 }
211 if let Some(def) = self.find_default()
212 && let (Some(login), Some(pass)) = (&def.login, &def.password)
213 {
214 return Some((login.clone(), pass.clone()));
215 }
216 None
217 }
218
219 pub fn entries(&self) -> &[NetRcEntry] {
220 &self.entries
221 }
222 pub fn default_entry(&self) -> Option<&NetRcEntry> {
223 self.default_entry.as_ref()
224 }
225 pub fn path(&self) -> Option<&str> {
226 self.path.as_deref()
227 }
228 pub fn len(&self) -> usize {
229 self.entries.len() + if self.default_entry.is_some() { 1 } else { 0 }
230 }
231 pub fn is_empty(&self) -> bool {
232 self.entries.is_empty() && self.default_entry.is_none()
233 }
234
235 pub fn to_map(&self) -> HashMap<String, (Option<String>, Option<String>)> {
236 let mut map = HashMap::new();
237 for entry in &self.entries {
238 map.insert(
239 entry.machine.clone(),
240 (entry.login.clone(), entry.password.clone()),
241 );
242 }
243 if let Some(def) = &self.default_entry {
244 map.insert("default".into(), (def.login.clone(), def.password.clone()));
245 }
246 map
247 }
248}
249
250impl Default for NetRcFile {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256pub fn find_netrc_file() -> Option<String> {
257 let home = std::env::var_os("HOME")
258 .or_else(|| std::env::var_os("USERPROFILE"))
259 .or_else(|| {
260 std::env::var_os("HOMEDRIVE").and_then(|d| {
261 std::env::var_os("HOMEPATH").map(|p| {
262 let mut s = d.to_os_string();
263 s.push(p);
264 s
265 })
266 })
267 });
268 home.and_then(|h| {
269 let h = h.to_string_lossy().to_string();
270 for name in &[".netrc", "_netrc", ".netrc.txt"] {
271 let candidate = format!("{}/{}", h, name);
272 if Path::new(&candidate).exists() {
273 return Some(candidate);
274 }
275 }
276 None
277 })
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_parse_basic_machine() {
286 let content = "machine ftp.example.com\nlogin myuser\npassword mypass\n";
287 let mut netrc = NetRcFile::new();
288 netrc.parse(content).unwrap();
289 assert_eq!(netrc.len(), 1);
290 let entry = &netrc.entries()[0];
291 assert_eq!(entry.machine, "ftp.example.com");
292 assert_eq!(entry.login.as_deref(), Some("myuser"));
293 assert_eq!(entry.password.as_deref(), Some("mypass"));
294 }
295
296 #[test]
297 fn test_parse_default_entry() {
298 let content = "default\nlogin anonymous\npassword guest@\n";
299 let mut netrc = NetRcFile::new();
300 netrc.parse(content).unwrap();
301 assert_eq!(netrc.len(), 1);
302 assert!(netrc.default_entry().is_some());
303 let def = netrc.default_entry().unwrap();
304 assert_eq!(def.machine, "default");
305 assert_eq!(def.login.as_deref(), Some("anonymous"));
306 }
307
308 #[test]
309 fn test_parse_multiple_machines() {
310 let content = r#"machine ftp.example.com
311login user1
312pass pass1
313
314machine ssh.example.com
315login user2
316password pass2
317"#;
318 let mut netrc = NetRcFile::new();
319 netrc.parse(content).unwrap();
320 assert_eq!(netrc.len(), 2);
321 assert_eq!(netrc.entries()[0].machine, "ftp.example.com");
322 assert_eq!(netrc.entries()[1].machine, "ssh.example.com");
323 }
324
325 #[test]
326 fn test_find_exact_host() {
327 let content = "machine ftp.example.com\nlogin user\npassword pass\n";
328 let mut netrc = NetRcFile::new();
329 netrc.parse(content).unwrap();
330 let creds = netrc.get_credentials("ftp.example.com");
331 assert!(creds.is_some());
332 let (u, p) = creds.unwrap();
333 assert_eq!(u, "user");
334 assert_eq!(p, "pass");
335 }
336
337 #[test]
338 fn test_find_unknown_host_returns_none() {
339 let content = "machine ftp.example.com\nlogin user\npassword pass\n";
340 let mut netrc = NetRcFile::new();
341 netrc.parse(content).unwrap();
342 assert!(netrc.get_credentials("unknown.host.com").is_none());
343 }
344
345 #[test]
346 fn test_find_falls_back_to_default() {
347 let content = r#"default
348login anon
349password guest@
350
351machine special.example.com
352login admin
353password secret
354"#;
355 let mut netrc = NetRcFile::new();
356 netrc.parse(content).unwrap();
357 let creds = netrc.get_credentials("other.example.com");
358 assert!(creds.is_some());
359 let (u, _p) = creds.unwrap();
360 assert_eq!(u, "anon");
361 }
362
363 #[test]
364 fn test_has_credentials() {
365 let mut entry = NetRcEntry::new("host");
366 entry.login = Some("user".into());
367 entry.password = Some("pass".into());
368 assert!(entry.has_credentials());
369
370 let mut entry2 = NetRcEntry::new("host");
371 entry2.login = Some("user".into());
372 assert!(!entry2.has_credentials());
373 }
374
375 #[test]
376 fn test_comments_and_blank_lines() {
377 let content = r#"
378# This is a comment
379machine example.com
380# Another comment
381login user
382 password pass
383"#;
384 let mut netrc = NetRcFile::new();
385 netrc.parse(content).unwrap();
386 assert_eq!(netrc.len(), 1);
387 assert_eq!(netrc.entries()[0].login.as_deref(), Some("user"));
388 }
389
390 #[test]
391 fn test_passwd_alias() {
392 let content = "machine example.com\nlogin user\npasswd secret\n";
393 let mut netrc = NetRcFile::new();
394 netrc.parse(content).unwrap();
395 assert_eq!(netrc.entries()[0].password.as_deref(), Some("secret"));
396 }
397
398 #[test]
399 fn test_account_field() {
400 let content = "machine example.com\nlogin user\npassword pass\naccount acct123\n";
401 let mut netrc = NetRcFile::new();
402 netrc.parse(content).unwrap();
403 assert_eq!(netrc.entries()[0].account.as_deref(), Some("acct123"));
404 }
405
406 #[test]
407 fn test_to_map() {
408 let content = "machine host1\nlogin u1\npassword p1\ndefault\nlogin du\npassword dp\n";
409 let mut netrc = NetRcFile::new();
410 netrc.parse(content).unwrap();
411 let map = netrc.to_map();
412 assert!(map.contains_key("host1"));
413 assert!(map.contains_key("default"));
414 }
415
416 #[test]
417 fn test_empty_netrc() {
418 let netrc = NetRcFile::new();
419 assert!(netrc.is_empty());
420 assert_eq!(netrc.len(), 0);
421 }
422
423 #[test]
424 fn test_error_display() {
425 let err = NetRcError::FileNotFound("/missing/.netrc".into());
426 assert!(err.to_string().contains(".netrc"));
427
428 let err2 = NetRcError::ParseError("bad token".into());
429 assert!(err2.to_string().contains("parse error"));
430 }
431
432 #[test]
433 fn test_case_insensitive_keywords() {
434 let content = "MACHINE example.com\nLOGIN user\nPASSWORD pass\n";
435 let mut netrc = NetRcFile::new();
436 netrc.parse(content).unwrap();
437 assert_eq!(netrc.entries()[0].login.as_deref(), Some("user"));
438 }
439}