use crate::{
authentication_storage::{AuthenticationStorageError, StorageBackend},
Authentication,
};
use netrc_rs::{Machine, Netrc};
use std::{collections::HashMap, env, io::ErrorKind, path::Path, path::PathBuf};
fn default_netrc_path() -> PathBuf {
let Some(mut path) = dirs::home_dir() else {
return PathBuf::from(".netrc");
};
#[cfg(windows)]
path.push("_netrc");
#[cfg(not(windows))]
path.push(".netrc");
path
}
#[derive(Debug, Clone, Default)]
pub struct NetRcStorage {
machines: HashMap<String, Machine>,
}
#[derive(thiserror::Error, Debug)]
pub enum NetRcStorageError {
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error("could not parse .netrc file: {0}")]
ParseError(netrc_rs::Error),
#[error("{0}")]
NotSupportedError(String),
}
impl NetRcStorage {
pub fn from_env() -> Result<Self, (PathBuf, NetRcStorageError)> {
let (path, explicit) = if let Ok(val) = env::var("NETRC") {
tracing::debug!(
"\"NETRC\" environment variable set, using netrc file at {}",
val
);
(PathBuf::from(val), true)
} else {
(default_netrc_path(), false)
};
match Self::from_path(&path) {
Ok(storage) => Ok(storage),
Err(NetRcStorageError::IOError(err))
if err.kind() == ErrorKind::NotFound && !explicit =>
{
Ok(Self::default())
}
Err(err) => Err((path, err)),
}
}
pub fn from_path(path: &Path) -> Result<Self, NetRcStorageError> {
let content = std::fs::read_to_string(path)?;
let netrc = Netrc::parse(content, false).map_err(NetRcStorageError::ParseError)?;
let machines = netrc
.machines
.into_iter()
.map(|m| (m.name.clone(), m))
.filter_map(|(name, value)| name.map(|n| (n, value)))
.collect();
Ok(Self { machines })
}
pub fn get_password(&self, host: &str) -> Result<Option<Authentication>, NetRcStorageError> {
match self.machines.get(host) {
Some(machine) => Ok(Some(Authentication::BasicHTTP {
username: machine.login.clone().unwrap_or_default(),
password: machine.password.clone().unwrap_or_default(),
})),
None => Ok(None),
}
}
}
impl StorageBackend for NetRcStorage {
fn store(
&self,
_host: &str,
_authentication: &Authentication,
) -> Result<(), AuthenticationStorageError> {
Err(NetRcStorageError::NotSupportedError(
"NetRcStorage does not support storing credentials".to_string(),
))?
}
fn delete(&self, _host: &str) -> Result<(), AuthenticationStorageError> {
Err(NetRcStorageError::NotSupportedError(
"NetRcStorage does not support deleting credentials".to_string(),
))?
}
fn get(&self, host: &str) -> Result<Option<Authentication>, AuthenticationStorageError> {
match self.get_password(host) {
Ok(Some(auth)) => Ok(Some(auth)),
Ok(None) => Ok(None),
Err(err) => Err(err.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AuthenticationStorage;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_file_storage() {
let file = tempdir().unwrap();
let path = file.path().join(".testnetrc");
let mut netrc = std::fs::File::create(&path).unwrap();
netrc
.write_all(b"machine mainmachine\nlogin test\npassword password\n")
.unwrap();
netrc.flush().unwrap();
let storage = NetRcStorage::from_path(path.as_path()).unwrap();
assert_eq!(
storage.get("mainmachine").unwrap(),
Some(Authentication::BasicHTTP {
username: "test".to_string(),
password: "password".to_string(),
})
);
assert_eq!(storage.get("test_unknown").unwrap(), None);
}
#[test]
fn test_file_storage_from_env() {
let file = tempdir().unwrap();
let path = file.path().join(".testnetrc2");
let mut netrc = std::fs::File::create(&path).unwrap();
netrc
.write_all(b"machine supermachine\nlogin test2\npassword password2\n")
.unwrap();
netrc.flush().unwrap();
let old_netrc = env::var("NETRC");
env::set_var("NETRC", path.as_os_str());
let storage = NetRcStorage::from_env().unwrap();
assert_eq!(
storage.get("supermachine").unwrap(),
Some(Authentication::BasicHTTP {
username: "test2".to_string(),
password: "password2".to_string(),
})
);
assert_eq!(storage.get("test_unknown").unwrap(), None);
if let Ok(netrc) = old_netrc {
env::set_var("NETRC", netrc);
} else {
env::remove_var("NETRC");
}
}
#[test]
fn test_from_env_malformed_netrc_returns_err() {
let dir = tempdir().unwrap();
let path = dir.path().join(".netrc-malformed");
std::fs::write(&path, b"this is not a valid netrc file !!!!").unwrap();
temp_env::with_var("NETRC", Some(path.as_os_str()), || {
let result = NetRcStorage::from_env();
assert!(
result.is_err(),
"expected malformed netrc file to surface an error so the \
caller can log a warning",
);
});
}
#[test]
fn test_from_env_missing_explicit_netrc_returns_err() {
let dir = tempdir().unwrap();
let missing = dir.path().join("does-not-exist.netrc");
assert!(!missing.exists());
temp_env::with_var("NETRC", Some(missing.as_os_str()), || {
let result = NetRcStorage::from_env();
assert!(
result.is_err(),
"explicit NETRC pointing at a missing file must return Err",
);
});
}
#[test]
#[tracing_test::traced_test]
fn test_from_env_and_defaults_warns_on_malformed_netrc() {
let dir = tempdir().unwrap();
let path = dir.path().join(".netrc-malformed");
std::fs::write(&path, b"this is not a valid netrc file !!!!").unwrap();
temp_env::with_vars(
[
("NETRC", Some(path.as_os_str())),
("RATTLER_AUTH_FILE", None),
],
|| {
let _ = AuthenticationStorage::from_env_and_defaults();
},
);
assert!(
logs_contain("error reading netrc file"),
"expected a tracing::warn! about the malformed netrc file",
);
}
#[test]
#[tracing_test::traced_test]
fn test_from_env_and_defaults_warns_on_missing_explicit_netrc() {
let dir = tempdir().unwrap();
let missing = dir.path().join("does-not-exist.netrc");
temp_env::with_vars(
[
("NETRC", Some(missing.as_os_str())),
("RATTLER_AUTH_FILE", None),
],
|| {
let _ = AuthenticationStorage::from_env_and_defaults();
},
);
assert!(
logs_contain("error reading netrc file"),
"no tracing::warn! was emitted for an explicitly-set NETRC that \
points to a missing file — this is the bug: `from_env` swallows \
ErrorKind::NotFound even when the path comes from the env var",
);
}
}