use log::{debug, warn};
use std::io::{BufRead, BufReader, Read, Write};
use url::Url;
use snafu::{ResultExt, Snafu};
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Could not read from reader: {}", source))]
ReadError {
source: std::io::Error,
},
#[snafu(display("Could not write to writer: {}", source))]
WriteError {
source: std::io::Error,
},
#[snafu(display("Could not parse the git-credential format: {}", source))]
ParseError {
value: String,
source: url::ParseError,
},
}
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct GitCredential {
pub url: Option<Url>,
pub protocol: Option<String>,
pub host: Option<String>,
pub path: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
}
impl Default for GitCredential {
fn default() -> GitCredential {
GitCredential {
url: None,
protocol: None,
host: None,
path: None,
username: None,
password: None,
}
}
}
impl GitCredential {
pub fn from_reader(source: impl Read) -> Result<GitCredential> {
let mut gc = GitCredential::default();
let source = BufReader::new(source);
for line in source.lines() {
let line = line.context(ReadSnafu {})?;
if line.is_empty() {
break;
}
match line.split_terminator('=').collect::<Vec<&str>>().as_slice() {
[key, value] => {
debug!("Reading line with: {} = {}", key, value);
let value = (*value).to_string();
let key = key.to_owned(); match key {
"url" => {
gc.url = {
let value = Url::parse(&value).context(ParseSnafu { value })?;
Some(value)
}
}
"protocol" => gc.protocol = Some(value),
"host" => gc.host = Some(value),
"path" => gc.path = Some(value),
"username" => gc.username = Some(value),
"password" => gc.password = Some(value),
_ => warn!("Unknown key: {} = {}", &key, &value),
};
}
_ => warn!("Invalid line: {}", &line),
};
}
Ok(gc)
}
pub fn to_writer(&self, mut sink: impl Write) -> Result<()> {
if let Some(url) = &self.url {
writeln!(sink, "url={}", url).context(WriteSnafu)?;
}
if let Some(protocol) = &self.protocol {
writeln!(sink, "protocol={}", protocol).context(WriteSnafu)?;
}
if let Some(host) = &self.host {
writeln!(sink, "host={}", host).context(WriteSnafu)?;
}
if let Some(path) = &self.path {
writeln!(sink, "path={}", path).context(WriteSnafu)?;
}
if let Some(username) = &self.username {
writeln!(sink, "username={}", username).context(WriteSnafu)?;
}
if let Some(password) = &self.password {
writeln!(sink, "password={}", password).context(WriteSnafu)?;
}
writeln!(sink).context(WriteSnafu)?;
Ok(())
}
}
#[cfg(doctest)]
doc_comment::doctest!("../README.md");
#[cfg(test)]
mod tests {
use super::{GitCredential, Url};
#[test]
fn read_from_reader() {
let s = "username=me\npassword=%sec&ret!\nprotocol=https\nhost=example.com\npath=myproject.git\nurl=https://example.com/myproject.git\n\n".as_bytes();
let g = GitCredential::from_reader(s).unwrap();
assert_eq!(g.username.unwrap(), "me");
assert_eq!(g.password.unwrap(), "%sec&ret!");
assert_eq!(g.protocol.unwrap(), "https");
assert_eq!(g.host.unwrap(), "example.com");
assert_eq!(g.path.unwrap(), "myproject.git");
assert_eq!(
g.url.unwrap(),
Url::parse("https://example.com/myproject.git").unwrap()
);
}
#[test]
fn write_to_writer() {
let s = "url=https://example.com/myproject.git\nprotocol=https\nhost=example.com\npath=myproject.git\nusername=me\npassword=%sec&ret!\n\n";
let mut g = GitCredential::default();
g.username = Some("me".into());
g.password = Some("%sec&ret!".into());
g.url = Some(Url::parse("https://example.com/myproject.git").unwrap());
g.protocol = Some("https".into());
g.host = Some("example.com".into());
g.path = Some("myproject.git".into());
let mut v: Vec<u8> = Vec::new();
g.to_writer(&mut v).unwrap();
assert_eq!(s, String::from_utf8(v).unwrap());
}
#[test]
fn read_and_write_adain() {
let s = "url=https://example.com/myproject.git\nprotocol=https\nhost=example.com\npath=myproject.git\nusername=me\npassword=%sec&ret!\n\n";
let g = GitCredential::from_reader(s.as_bytes()).unwrap();
let mut v: Vec<u8> = Vec::new();
g.to_writer(&mut v).unwrap();
assert_eq!(s, String::from_utf8(v).unwrap());
}
}