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}