use std::{
fs::File,
io::{self, BufRead, BufReader},
};
use encoding_rs::UTF_8;
use encoding_rs_io::DecodeReaderBytesBuilder;
use crate::utils::get_home_dir;
#[derive(Debug, PartialEq, Eq)]
pub struct Entry {
pub login: Option<String>,
pub password: String,
}
pub fn find_entry(host: url::Host<&str>) -> Option<Entry> {
let file = open_netrc()?;
let file = DecodeReaderBytesBuilder::new()
.encoding(Some(UTF_8))
.bom_override(true)
.build(file);
let file = BufReader::new(file);
let parser = Parser::new(file, host);
parser.parse().ok()?
}
fn open_netrc() -> Option<File> {
match std::env::var_os("NETRC") {
Some(path) => File::open(path).ok(),
None => {
let home_dir = get_home_dir()?;
for name in [".netrc", "_netrc"] {
let path = home_dir.join(name);
if let Ok(file) = File::open(path) {
return Some(file);
}
}
None
}
}
}
#[derive(Copy, Clone)]
enum EntryState {
Wrong,
Correct,
Default,
}
struct Parser<'a, R> {
reader: R,
buf: String,
pos: usize,
host: url::Host<&'a str>,
state: EntryState,
login: Option<String>,
password: Option<String>,
account: Option<String>,
suppress_default: bool,
default: Option<Entry>,
entry: Option<Entry>,
}
impl<'a, R: BufRead> Parser<'a, R> {
fn new(reader: R, host: url::Host<&'a str>) -> Self {
Parser {
reader,
buf: String::new(),
pos: 0,
host,
state: EntryState::Wrong,
login: None,
password: None,
account: None,
suppress_default: false,
default: None,
entry: None,
}
}
fn parse(mut self) -> io::Result<Option<Entry>> {
while let Some(word) = self.word()? {
match word {
"default" => {
self.finish_entry();
self.state = EntryState::Default;
}
"machine" => {
self.finish_entry();
if let Some(new_host) = self.word()? {
match url::Host::parse(new_host) {
Ok(new_host) if self.host == new_host => {
self.state = EntryState::Correct;
self.suppress_default = true;
}
_ => {
self.state = EntryState::Wrong;
}
}
}
}
"login" => {
if let Some(login) = self.arg()? {
self.login = Some(login);
}
}
"password" => {
if let Some(password) = self.arg()? {
self.password = Some(password);
}
}
"account" => {
if let Some(account) = self.arg()? {
self.account = Some(account);
}
}
"macdef" => {
self.finish_entry();
self.word()?;
self.advance_line()?;
while !self.buf.trim().is_empty() {
self.advance_line()?;
}
}
word if word.starts_with('#') => {
self.advance_line()?;
}
_ => {
self.finish_entry();
}
}
if let Some(entry) = self.entry {
return Ok(Some(entry));
}
}
self.finish_entry();
if let Some(entry) = self.entry {
Ok(Some(entry))
} else if self.suppress_default {
Ok(None)
} else {
Ok(self.default)
}
}
fn finish_entry(&mut self) {
let login = self.login.take();
let account = self.account.take();
let password = self.password.take();
let state = self.state;
self.state = EntryState::Wrong;
if let (login, Some(password)) = (login.or(account), password) {
let entry = Entry { login, password };
match state {
EntryState::Wrong => unreachable!("netrc: Should not have been storing info"),
EntryState::Correct => self.entry = Some(entry),
EntryState::Default => self.default = Some(entry),
}
}
}
fn arg(&mut self) -> io::Result<Option<String>> {
let state = self.state;
let word = self.word()?;
match state {
EntryState::Wrong => Ok(None),
EntryState::Correct | EntryState::Default => Ok(word.map(str::to_owned)),
}
}
fn advance_line(&mut self) -> io::Result<usize> {
self.buf.clear();
self.pos = 0;
self.reader.read_line(&mut self.buf)
}
fn word(&mut self) -> io::Result<Option<&str>> {
loop {
match self.buf[self.pos..].chars().next() {
Some(ch) if ch.is_whitespace() => self.pos += ch.len_utf8(),
Some(_) => {
let text = self.buf[self.pos..].split_whitespace().next().unwrap();
self.pos += text.len();
return Ok(Some(text));
}
None => {
if self.advance_line()? == 0 {
return Ok(None);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn cases() {
const COM: url::Host<&str> = url::Host::Domain("example.com");
const ORG: url::Host<&str> = url::Host::Domain("example.org");
const UNI: url::Host<&str> = url::Host::Domain("xn--9ca.com");
const IP1: url::Host<&str> = url::Host::Ipv4(Ipv4Addr::new(1, 1, 1, 1));
const IP2: url::Host<&str> = url::Host::Ipv4(Ipv4Addr::new(2, 2, 2, 2));
const SIMPLE: &str = "
machine example.com
login user
password pass
";
found(SIMPLE, COM, "user", "pass");
notfound(SIMPLE, ORG);
notfound(SIMPLE, UNI);
notfound(SIMPLE, IP1);
const ONELINE: &str = "
machine example.com login user password pass
";
found(ONELINE, COM, "user", "pass");
notfound(ONELINE, ORG);
const MULTI: &str = "
machine example.com login user password pass
machine example.org login foo password bar
";
found(MULTI, COM, "user", "pass");
found(MULTI, ORG, "foo", "bar");
notfound(MULTI, UNI);
const UNICODE: &str = "
machine É.com login user password pass
";
found(UNICODE, UNI, "user", "pass");
notfound(UNICODE, COM);
const MISSING_PASS: &str = "
machine example.com login user
";
notfound(MISSING_PASS, COM);
const MISSING_USER: &str = "
machine example.com password pass
default login user
";
found(MISSING_USER, COM, None, "pass");
notfound(MISSING_USER, ORG);
const DEFAULT_PASSWORD_MISSING_USER: &str = "
machine example.com password pass
default password def
";
found(DEFAULT_PASSWORD_MISSING_USER, COM, None, "pass");
found(DEFAULT_PASSWORD_MISSING_USER, ORG, None, "def");
const DEFAULT_LAST: &str = "
machine example.com login ex password am
default login def password ault
";
found(DEFAULT_LAST, COM, "ex", "am");
found(DEFAULT_LAST, ORG, "def", "ault");
const DEFAULT_FIRST: &str = "
default login def password ault
machine example.com login ex password am
";
found(DEFAULT_FIRST, COM, "ex", "am");
found(DEFAULT_FIRST, ORG, "def", "ault");
const ACCOUNT_FALLBACK: &str = "
machine example.com account acc password pass
";
found(ACCOUNT_FALLBACK, COM, "acc", "pass");
const ACCOUNT_NOT_PREFERRED: &str = "
machine example.com password pass login log account acc
machine example.org password pass account acc login log
";
found(ACCOUNT_NOT_PREFERRED, COM, "log", "pass");
found(ACCOUNT_NOT_PREFERRED, ORG, "log", "pass");
const WITH_IP: &str = "
machine 1.1.1.1 login us password pa
";
found(WITH_IP, IP1, "us", "pa");
notfound(WITH_IP, IP2);
notfound(WITH_IP, COM);
const WEIRD_IP: &str = "
machine 16843009 login us password pa
";
found(WEIRD_IP, IP1, "us", "pa");
notfound(WEIRD_IP, IP2);
notfound(WEIRD_IP, COM);
const MALFORMED: &str = "
I'm a malformed netrc!
";
notfound(MALFORMED, COM);
const COMMENT: &str = "
# machine example.com login user password pass
machine example.org login lo password pa
";
notfound(COMMENT, COM);
found(COMMENT, ORG, "lo", "pa");
const OCTOTHORPE_IN_VALUE: &str = "
machine example.com login #!@$ password pass
";
found(OCTOTHORPE_IN_VALUE, COM, "#!@$", "pass");
const SUDDEN_END: &str = "
machine example.com login
";
notfound(SUDDEN_END, COM);
const INCOMPLETE_AND_DEFAULT: &str = "
machine example.com login user
default login u password p
";
notfound(INCOMPLETE_AND_DEFAULT, COM);
found(INCOMPLETE_AND_DEFAULT, ORG, "u", "p");
const UNKNOWN_TOKEN_INTERRUPT: &str = "
machine example.com
login user
foo bar
password pass
";
notfound(UNKNOWN_TOKEN_INTERRUPT, COM);
const MACRO: &str = "
macdef foo
machine example.com login mac password def
qux
machine example.com login user password pass
";
found(MACRO, COM, "user", "pass");
notfound(MACRO, ORG);
const MACRO_UNTERMINATED: &str = "
macdef foo
machine example.com login mac password def
qux
machine example.com login user password pass";
notfound(MACRO_UNTERMINATED, COM);
const MACRO_BLANK_LINE_BEFORE_NAME: &str = "
macdef
foo
machine example.com login mac password def";
notfound(MACRO_BLANK_LINE_BEFORE_NAME, COM);
const MANY_LINES: &str = "
machine
example.com
login
user
password
pass
";
found(MANY_LINES, COM, "user", "pass");
const STRANGE_CHARACTERS: &str = "
machine\u{2029}oké\t\u{2029}login u password p\t\t\t\r\n
";
notfound(STRANGE_CHARACTERS, COM);
}
#[track_caller]
fn found(
netrc: &str,
host: url::Host<&str>,
login: impl Into<Option<&'static str>>,
password: &str,
) {
let entry = Parser::new(netrc.as_bytes(), host).parse().unwrap();
let entry = entry.expect("Didn't find entry");
assert_eq!(entry.login.as_deref(), login.into());
assert_eq!(entry.password, password);
}
#[track_caller]
fn notfound(netrc: &str, host: url::Host<&str>) {
let entry = Parser::new(netrc.as_bytes(), host).parse().unwrap();
assert!(entry.is_none(), "Found entry");
}
}