Skip to main content

uv_netrc/
lib.rs

1/*!
2The `uv-netrc` crate provides a parser for the netrc file.
3
4# Setup
5
6```text
7$ cargo add uv-netrc
8```
9
10# Example
11
12```ignore
13use uv_netrc::Netrc;
14
15// ...
16
17let nrc = Netrc::new().unwrap();
18
19// ...
20println!(
21    "login = {}\naccount = {}\npassword = {}",
22    nrc.hosts["my.host"].login,
23    nrc.hosts["my.host"].account,
24    nrc.hosts["my.host"].password,
25);
26```
27
28*/
29
30pub 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/// An error that can occur when processing a Netrc file.
45#[derive(thiserror::Error, Debug)]
46pub enum Error {
47    /// Wrap `std::io::Error` when we try to open the netrc file.
48    #[error("I/O error: {0}")]
49    Io(#[from] std::io::Error),
50
51    /// Parsing error.
52    #[error("{parser} in the file '{filename}'")]
53    Parsing {
54        parser: netrc::ParsingError,
55        filename: String,
56    },
57}
58
59impl Netrc {
60    /// Create a new `Netrc` object.
61    ///
62    /// Look up the `NETRC` environment variable if it is defined else that the
63    /// default `~/.netrc` file.
64    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    /// Create a new `Netrc` object from a file.
74    #[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    /// Search a netrc file.
88    ///
89    /// Look up the `NETRC` environment variable if it is defined else use the .netrc (or _netrc
90    /// file on windows) in the user's home directory.
91    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}