git_credential/lib.rs
1// Copyright 2019 Pascal Bach.
2//
3// SPDX-License-Identifier: Apache-2.0 or MIT
4
5//! The git_credentials crate provides types that help to implement git-credential helpers.
6//!
7//! The format is documented in [git-credential[1] ](https://git-scm.com/docs/git-credential)
8//!
9//! The library is intended to help creating custom git credential helpers.
10//!
11//! See [gitcredentials[7]](https://git-scm.com/docs/gitcredentials) for more information on how to use git credential helpers.
12//!
13//! See [api-credentials[7]](https://git-scm.com/docs/api-credentials#_credential_helpers) for more details on how to write your own.
14use log::{debug, warn};
15use std::io::{BufRead, BufReader, Read, Write};
16use url::Url;
17
18use snafu::{ResultExt, Snafu};
19
20/// Errors that can occur while reading or writing the git credential format
21#[derive(Debug, Snafu)]
22pub enum Error {
23 /// Indicates that there was an error while reading from the given Reader
24 #[snafu(display("Could not read from reader: {}", source))]
25 ReadError {
26 /// The underlying io error causing the issue
27 source: std::io::Error,
28 },
29 /// Indicates that there was an error while writing to the given Writer
30 #[snafu(display("Could not write to writer: {}", source))]
31 WriteError {
32 /// The underlying io error causing the issue
33 source: std::io::Error,
34 },
35 /// Indicates that the format received did not correspond to the one specified in [git-credential[1] ](https://git-scm.com/docs/git-credential).
36 #[snafu(display("Could not parse the git-credential format: {}", source))]
37 ParseError {
38 /// The value that could not be parsed
39 value: String,
40 /// The underlying io error causing the issue
41 source: url::ParseError,
42 },
43}
44
45type Result<T, E = Error> = std::result::Result<T, E>;
46
47/// Holds the values of all parameters supported by git-credential
48#[derive(Debug)]
49pub struct GitCredential {
50 /// The url field is treated specially by git-credential.
51 /// Setting the url corresponds to setting all the other fields as part of the url.
52 ///
53 /// The url has the following format: `<protocol>://<username>:<password>@<host>/<path>`.
54 pub url: Option<Url>,
55 /// The protocol over which the credential will be used (e.g., `https`).
56 pub protocol: Option<String>,
57 /// The remote hostname for a network credential (e.g., `example.com`).
58 pub host: Option<String>,
59 /// The path with which the credential will be used. E.g., for accessing a remote https repository, this will be the repository’s path on the server.
60 pub path: Option<String>,
61 /// The credential’s username, if we already have one (e.g., from a URL, from the user, or from a previously run helper).
62 pub username: Option<String>,
63 /// The credential’s password, if we are asking it to be stored.
64 pub password: Option<String>,
65}
66
67impl Default for GitCredential {
68 /// Creates a new GitCredential struct with all values set to None
69 fn default() -> GitCredential {
70 GitCredential {
71 url: None,
72 protocol: None,
73 host: None,
74 path: None,
75 username: None,
76 password: None,
77 }
78 }
79}
80
81impl GitCredential {
82 /// Read the git-credential values from a Reader like stdin
83 ///
84 /// ```
85 /// use git_credential::GitCredential;
86 ///
87 /// let s = "username=me\npassword=%sec&ret!\n\n".as_bytes();
88 ///
89 /// let g = GitCredential::from_reader(s).unwrap();
90 ///
91 /// assert_eq!(g.username.unwrap(), "me");
92 /// assert_eq!(g.password.unwrap(), "%sec&ret!");
93 /// ```
94 pub fn from_reader(source: impl Read) -> Result<GitCredential> {
95 let mut gc = GitCredential::default();
96 let source = BufReader::new(source);
97 for line in source.lines() {
98 let line = line.context(ReadSnafu {})?;
99 if line.is_empty() {
100 // An empty line means we are done
101 // TODO: Make sure an empty line exists in the end
102 break;
103 }
104 match line.split_terminator('=').collect::<Vec<&str>>().as_slice() {
105 [key, value] => {
106 debug!("Reading line with: {} = {}", key, value);
107 let value = (*value).to_string();
108 let key = key.to_owned(); // TODO: Why is this needed?
109 match key {
110 "url" => {
111 gc.url = {
112 let value = Url::parse(&value).context(ParseSnafu { value })?;
113 Some(value)
114 }
115 }
116 "protocol" => gc.protocol = Some(value),
117 "host" => gc.host = Some(value),
118 "path" => gc.path = Some(value),
119 "username" => gc.username = Some(value),
120 "password" => gc.password = Some(value),
121 _ => warn!("Unknown key: {} = {}", &key, &value),
122 };
123 }
124 _ => warn!("Invalid line: {}", &line),
125 };
126 }
127 Ok(gc)
128 }
129
130 /// Writes the git-credentials values to a Writer like stdout
131 ///
132 /// ```
133 /// use git_credential::GitCredential;
134 ///
135 /// let mut g = GitCredential::default();
136 /// g.username = Some("me".into());
137 /// g.password = Some("%sec&ret!".into());
138 ///
139 /// let mut v: Vec<u8> = Vec::new();
140 ///
141 /// g.to_writer(&mut v).unwrap();
142 ///
143 /// assert_eq!("username=me\npassword=%sec&ret!\n\n", String::from_utf8(v).unwrap());
144 /// ```
145 pub fn to_writer(&self, mut sink: impl Write) -> Result<()> {
146 // The url filed is written first, this allows the other fields to override
147 // parts of the url
148 if let Some(url) = &self.url {
149 writeln!(sink, "url={}", url).context(WriteSnafu)?;
150 }
151 if let Some(protocol) = &self.protocol {
152 writeln!(sink, "protocol={}", protocol).context(WriteSnafu)?;
153 }
154 if let Some(host) = &self.host {
155 writeln!(sink, "host={}", host).context(WriteSnafu)?;
156 }
157 if let Some(path) = &self.path {
158 writeln!(sink, "path={}", path).context(WriteSnafu)?;
159 }
160 if let Some(username) = &self.username {
161 writeln!(sink, "username={}", username).context(WriteSnafu)?;
162 }
163 if let Some(password) = &self.password {
164 writeln!(sink, "password={}", password).context(WriteSnafu)?;
165 }
166
167 // One empty line in the end
168 writeln!(sink).context(WriteSnafu)?;
169 Ok(())
170 }
171}
172
173// Make sure the readme is tested too
174#[cfg(doctest)]
175doc_comment::doctest!("../README.md");
176
177#[cfg(test)]
178mod tests {
179 use super::{GitCredential, Url};
180 #[test]
181 fn read_from_reader() {
182 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();
183 let g = GitCredential::from_reader(s).unwrap();
184 assert_eq!(g.username.unwrap(), "me");
185 assert_eq!(g.password.unwrap(), "%sec&ret!");
186 assert_eq!(g.protocol.unwrap(), "https");
187 assert_eq!(g.host.unwrap(), "example.com");
188 assert_eq!(g.path.unwrap(), "myproject.git");
189 assert_eq!(
190 g.url.unwrap(),
191 Url::parse("https://example.com/myproject.git").unwrap()
192 );
193 }
194
195 #[test]
196 fn write_to_writer() {
197 let s = "url=https://example.com/myproject.git\nprotocol=https\nhost=example.com\npath=myproject.git\nusername=me\npassword=%sec&ret!\n\n";
198 let mut g = GitCredential::default();
199 g.username = Some("me".into());
200 g.password = Some("%sec&ret!".into());
201 g.url = Some(Url::parse("https://example.com/myproject.git").unwrap());
202 g.protocol = Some("https".into());
203 g.host = Some("example.com".into());
204 g.path = Some("myproject.git".into());
205 let mut v: Vec<u8> = Vec::new();
206 g.to_writer(&mut v).unwrap();
207 assert_eq!(s, String::from_utf8(v).unwrap());
208 }
209
210 #[test]
211 fn read_and_write_adain() {
212 let s = "url=https://example.com/myproject.git\nprotocol=https\nhost=example.com\npath=myproject.git\nusername=me\npassword=%sec&ret!\n\n";
213 let g = GitCredential::from_reader(s.as_bytes()).unwrap();
214 let mut v: Vec<u8> = Vec::new();
215 g.to_writer(&mut v).unwrap();
216 assert_eq!(s, String::from_utf8(v).unwrap());
217 }
218}