Skip to main content

aria2_core/config/
netrc.rs

1use 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}