That way additional information, like from configuration, can be passed as well.
Unified · Split
| @@ -15,26 +15,25 @@ pub type Result = std::result::Result<Option<Outcome>, Error>; · 14 unmodified lines | ||
| 15 | 15 | #[derive(Debug, thiserror::Error)] |
| 16 | 16 | #[allow(missing_docs)] |
| 17 | 17 | pub enum Error { |
| 18 | − #[error("Cannot handle usernames or passwords in illformed UTF-8 encoding")] | |
| 19 | − IllformedUtf8InUsernameOrPassword, | |
| 18 | + #[error(transparent)] | |
| 19 | + Context(#[from] crate::helper::context::decode::Error), | |
| 20 | 20 | #[error("An IO error occurred while communicating to the credentials helper")] |
| 21 | 21 | Io(#[from] std::io::Error), |
| 22 | 22 | #[error("Could not find {name:?} in output of credentials helper")] |
| 23 | − KeyNotFound { name: String }, | |
| 23 | + KeyNotFound { name: &'static str }, | |
| 24 | 24 | #[error(transparent)] |
| 25 | 25 | CredentialsHelperFailed { source: std::io::Error }, |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | 28 | pub(crate) mod function { |
| 29 | − use crate::helper::{invoke::Error, invoke::Outcome, invoke::Result, message, Action, Context, NextAction}; | |
| 30 | − use bstr::ByteVec; | |
| 29 | + use crate::helper::{invoke::Error, invoke::Outcome, invoke::Result, Action, Context, NextAction}; | |
| 31 | 30 | use std::io::Read; |
| 32 | 31 | |
| 33 | − impl Action<'_> { | |
| 32 | + impl Action { | |
| 34 | 33 | /// Send ourselves to the given `write` which is expected to be credentials-helper compatible |
| 35 | 34 | pub fn send(&self, mut write: impl std::io::Write) -> std::io::Result<()> { |
| 36 | 35 | match self { |
| 37 | − Action::Fill(url) => message::encode(url, write), | |
| 36 | + Action::Fill(ctx) => ctx.write_to(write), | |
| 38 | 37 | Action::Approve(last) | Action::Reject(last) => { |
| 39 | 38 | write.write_all(last)?; |
| 40 | 39 | write.write_all(&[b'\n']) |
| @@ -47,7 +46,7 @@ pub(crate) mod function { · 6 unmodified lines | ||
| 47 | 46 | /// |
| 48 | 47 | /// Usually the first call is performed with [`Action::Fill`] to obtain an identity, which subsequently can be used. |
| 49 | 48 | /// On successful usage, use [`NextAction::approve()`], otherwise [`NextAction::reject()`]. |
| 50 | − pub fn invoke(mut helper: impl crate::Helper, action: Action<'_>, _context: Context) -> Result { | |
| 49 | + pub fn invoke(mut helper: impl crate::Helper, action: Action) -> Result { | |
| 51 | 50 | let (stdin, stdout) = helper.start(&action)?; |
| 52 | 51 | action.send(stdin)?; |
| 53 | 52 | let stdout = stdout |
| @@ -67,22 +66,11 @@ pub(crate) mod function { · 13 unmodified lines | ||
| 67 | 66 | match stdout { |
| 68 | 67 | None => Ok(None), |
| 69 | 68 | Some(stdout) => { |
| 70 | − let kvs = message::decode(stdout.as_slice())?; | |
| 71 | − let find = |name: &str| { | |
| 72 | − kvs.iter() | |
| 73 | − .find(|(k, _)| k == name) | |
| 74 | − .ok_or_else(|| Error::KeyNotFound { name: name.into() }) | |
| 75 | − .map(|(_, n)| n.to_vec()) | |
| 76 | − }; | |
| 69 | + let ctx = Context::from_bytes(stdout.as_slice())?; | |
| 70 | + let username = ctx.username.ok_or_else(|| Error::KeyNotFound { name: "username" })?; | |
| 71 | + let password = ctx.password.ok_or_else(|| Error::KeyNotFound { name: "password" })?; | |
| 77 | 72 | Ok(Some(Outcome { |
| 78 | − identity: git_sec::identity::Account { | |
| 79 | − username: find("username")? | |
| 80 | − .into_string() | |
| 81 | − .map_err(|_| Error::IllformedUtf8InUsernameOrPassword)?, | |
| 82 | − password: find("password")? | |
| 83 | − .into_string() | |
| 84 | − .map_err(|_| Error::IllformedUtf8InUsernameOrPassword)?, | |
| 85 | − }, | |
| 73 | + identity: git_sec::identity::Account { username, password }, | |
| 86 | 74 | next: NextAction { |
| 87 | 75 | previous_output: stdout.into(), |
| 88 | 76 | }, |
| @@ -1,4 +1,4 @@ | ||
| 1 | −use bstr::{BStr, BString}; | |
| 1 | +use bstr::BString; | |
| 2 | 2 | |
| 3 | 3 | /// The kind of helper program to use. |
| 4 | 4 | pub enum Kind { |
| @@ -31,16 +31,16 @@ pub mod context; · 26 unmodified lines | ||
| 31 | 31 | |
| 32 | 32 | /// The action to perform by the credentials [helper][`crate::helper()`]. |
| 33 | 33 | #[derive(Clone, Debug)] |
| 34 | −pub enum Action<'a> { | |
| 35 | − /// Provide credentials using the given repository URL (as &str) as context and pre-parsed url information as seen in [`Context`]. | |
| 36 | − Fill(&'a BStr), | |
| 34 | +pub enum Action { | |
| 35 | + /// Provide credentials using the given repository context, which must include the repository url. | |
| 36 | + Fill(Context), | |
| 37 | 37 | /// Approve the credentials as identified by the previous input provided as `BString`, containing information from [`Context`]. |
| 38 | 38 | Approve(BString), |
| 39 | 39 | /// Reject the credentials as identified by the previous input provided as `BString`. containing information from [`Context`]. |
| 40 | 40 | Reject(BString), |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | −impl<'a> Action<'a> { | |
| 43 | +impl Action { | |
| 44 | 44 | /// Returns true if this action expects output from the helper. |
| 45 | 45 | pub fn expects_output(&self) -> bool { |
| 46 | 46 | matches!(self, Action::Fill(_)) |
| @@ -67,11 +67,11 @@ pub struct NextAction { · 20 unmodified lines | ||
| 67 | 67 | |
| 68 | 68 | impl NextAction { |
| 69 | 69 | /// Approve the result of the previous [Action]. |
| 70 | − pub fn approve(self) -> Action<'static> { | |
| 70 | + pub fn approve(self) -> Action { | |
| 71 | 71 | Action::Approve(self.previous_output) |
| 72 | 72 | } |
| 73 | 73 | /// Reject the result of the previous [Action]. |
| 74 | − pub fn reject(self) -> Action<'static> { | |
| 74 | + pub fn reject(self) -> Action { | |
| 75 | 75 | Action::Reject(self.previous_output) |
| 76 | 76 | } |
| 77 | 77 | } |
| @@ -79,45 +79,3 @@ impl NextAction { · 1 unmodified line | ||
| 79 | 79 | /// |
| 80 | 80 | pub mod invoke; |
| 81 | 81 | pub use invoke::function::invoke; |
| 82 | − | |
| 83 | −/// | |
| 84 | −pub mod message { | |
| 85 | − use bstr::{BStr, BString, ByteSlice}; | |
| 86 | − | |
| 87 | − /// Encode `url` to `out` for consumption by a `git credentials` helper program. | |
| 88 | − pub fn encode(url: &BStr, mut out: impl std::io::Write) -> std::io::Result<()> { | |
| 89 | − validate(url)?; | |
| 90 | − writeln!(out, "url={}\n", url) | |
| 91 | − } | |
| 92 | − | |
| 93 | − fn validate(url: &BStr) -> std::io::Result<()> { | |
| 94 | − if url.contains(&0) || url.contains(&b'\n') { | |
| 95 | − return Err(std::io::Error::new( | |
| 96 | − std::io::ErrorKind::Other, | |
| 97 | − "token to encode must not contain newlines or null bytes", | |
| 98 | − )); | |
| 99 | − } | |
| 100 | − Ok(()) | |
| 101 | − } | |
| 102 | − | |
| 103 | − /// Decode all lines in `input` as key-value pairs produced by a `git credentials` helper program. | |
| 104 | − pub fn decode(mut input: impl std::io::Read) -> std::io::Result<Vec<(String, BString)>> { | |
| 105 | − let mut buf = Vec::<u8>::with_capacity(512); | |
| 106 | − input.read_to_end(&mut buf)?; | |
| 107 | − buf.lines() | |
| 108 | − .take_while(|line| !line.is_empty()) | |
| 109 | − .map(|line| { | |
| 110 | − let mut it = line.splitn(2, |b| *b == b'='); | |
| 111 | − match (it.next().and_then(|k| k.to_str().ok()), it.next().map(|v| v.as_bstr())) { | |
| 112 | − (Some(key), Some(value)) => validate(key.into()) | |
| 113 | − .and_then(|_| validate(value.into())) | |
| 114 | − .map(|_| (key.into(), value.into())), | |
| 115 | − _ => Err(std::io::Error::new( | |
| 116 | − std::io::ErrorKind::Other, | |
| 117 | − format!("Invalid format, expecting key=value, got {:?}", line.as_bstr()), | |
| 118 | − )), | |
| 119 | − } | |
| 120 | − }) | |
| 121 | − .collect::<std::io::Result<Vec<_>>>() | |
| 122 | − } | |
| 123 | −} | |
| @@ -17,7 +17,7 @@ pub trait Helper { · 16 unmodified lines | ||
| 17 | 17 | type Receive: std::io::Read; |
| 18 | 18 | |
| 19 | 19 | /// Start the helper and provide handles to send and receive from it. |
| 20 | − fn start(&mut self, action: &helper::Action<'_>) -> std::io::Result<(Self::Send, Option<Self::Receive>)>; | |
| 20 | + fn start(&mut self, action: &helper::Action) -> std::io::Result<(Self::Send, Option<Self::Receive>)>; | |
| 21 | 21 | /// Stop the helper and provide a way to determine it's successful. |
| 22 | 22 | fn finish(self) -> std::io::Result<()>; |
| 23 | 23 | } |
| @@ -38,10 +38,6 @@ pub mod helper; · 14 unmodified lines | ||
| 38 | 38 | /// Call the `git` credentials helper program performing the given `action`, without any context from git configuration. |
| 39 | 39 | /// |
| 40 | 40 | /// See [`invoke()`] for a more flexible implementation. |
| 41 | −pub fn helper(action: helper::Action<'_>) -> helper::invoke::Result { | |
| 42 | − helper::invoke( | |
| 43 | − Program::from_kind(helper::Kind::GitCredential), | |
| 44 | − action, | |
| 45 | − helper::Context::default(), | |
| 46 | − ) | |
| 41 | +pub fn helper(action: helper::Action) -> helper::invoke::Result { | |
| 42 | + helper::invoke(Program::from_kind(helper::Kind::GitCredential), action) | |
| 47 | 43 | } |
| @@ -12,7 +12,7 @@ impl Helper for Program { · 11 unmodified lines | ||
| 12 | 12 | type Send = std::process::ChildStdin; |
| 13 | 13 | type Receive = std::process::ChildStdout; |
| 14 | 14 | |
| 15 | − fn start(&mut self, action: &helper::Action<'_>) -> std::io::Result<(Self::Send, Option<Self::Receive>)> { | |
| 15 | + fn start(&mut self, action: &helper::Action) -> std::io::Result<(Self::Send, Option<Self::Receive>)> { | |
| 16 | 16 | match self { |
| 17 | 17 | Program::Ready(kind) => { |
| 18 | 18 | let (mut cmd, is_custom) = match kind { |
| @@ -86,70 +86,3 @@ username=bob"; · 85 unmodified lines | ||
| 86 | 86 | } |
| 87 | 87 | } |
| 88 | 88 | } |
| 89 | − | |
| 90 | −mod message { | |
| 91 | − mod encode { | |
| 92 | − use bstr::ByteSlice; | |
| 93 | − use git_credentials::helper::message; | |
| 94 | − | |
| 95 | − #[test] | |
| 96 | − fn from_url() -> crate::Result { | |
| 97 | − let mut out = Vec::new(); | |
| 98 | − message::encode("https://github.com/byron/gitoxide".into(), &mut out)?; | |
| 99 | − assert_eq!(out.as_bstr(), b"url=https://github.com/byron/gitoxide\n\n".as_bstr()); | |
| 100 | − Ok(()) | |
| 101 | − } | |
| 102 | − | |
| 103 | − mod invalid { | |
| 104 | − use git_credentials::helper::message; | |
| 105 | − use std::io; | |
| 106 | − | |
| 107 | − #[test] | |
| 108 | − fn contains_null() { | |
| 109 | − assert_eq!( | |
| 110 | − message::encode("https://foo\u{0}".into(), Vec::new()) | |
| 111 | − .err() | |
| 112 | − .map(|e| e.kind()), | |
| 113 | − Some(io::ErrorKind::Other) | |
| 114 | − ); | |
| 115 | − } | |
| 116 | − #[test] | |
| 117 | − fn contains_newline() { | |
| 118 | − assert_eq!( | |
| 119 | − message::encode("https://foo\n".into(), Vec::new()) | |
| 120 | − .err() | |
| 121 | − .map(|e| e.kind()), | |
| 122 | − Some(io::ErrorKind::Other) | |
| 123 | − ); | |
| 124 | − } | |
| 125 | − } | |
| 126 | − } | |
| 127 | − | |
| 128 | − mod decode { | |
| 129 | − use git_credentials::helper::message; | |
| 130 | − | |
| 131 | − #[test] | |
| 132 | − fn typical_response() -> crate::Result { | |
| 133 | − assert_eq!( | |
| 134 | − message::decode( | |
| 135 | − "protocol=https | |
| 136 | −host=example.com | |
| 137 | −username=bob | |
| 138 | −password=secr3t\n\n | |
| 139 | −this=is-skipped-past-empty-line" | |
| 140 | − .as_bytes() | |
| 141 | − )?, | |
| 142 | − vec![ | |
| 143 | − ("protocol", "https"), | |
| 144 | − ("host", "example.com"), | |
| 145 | − ("username", "bob"), | |
| 146 | − ("password", "secr3t") | |
| 147 | − ] | |
| 148 | − .iter() | |
| 149 | − .map(|(k, v)| (k.to_string(), (*v).into())) | |
| 150 | − .collect::<Vec<_>>() | |
| 151 | − ); | |
| 152 | − Ok(()) | |
| 153 | − } | |
| 154 | − } | |
| 155 | −} | |