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 | #[derive(Debug, thiserror::Error)] | 15 | #[derive(Debug, thiserror::Error)] |
| 16 | #[allow(missing_docs)] | 16 | #[allow(missing_docs)] |
| 17 | pub enum Error { | 17 | pub enum Error { |
| 18 | #[error("Cannot handle usernames or passwords in illformed UTF-8 encoding")] | 18 | #[error(transparent)] |
| 19 | IllformedUtf8InUsernameOrPassword, | 19 | Context(#[from] crate::helper::context::decode::Error), |
| 20 | #[error("An IO error occurred while communicating to the credentials helper")] | 20 | #[error("An IO error occurred while communicating to the credentials helper")] |
| 21 | Io(#[from] std::io::Error), | 21 | Io(#[from] std::io::Error), |
| 22 | #[error("Could not find {name:?} in output of credentials helper")] | 22 | #[error("Could not find {name:?} in output of credentials helper")] |
| 23 | KeyNotFound { name: String }, | 23 | KeyNotFound { name: &'static str }, |
| 24 | #[error(transparent)] | 24 | #[error(transparent)] |
| 25 | CredentialsHelperFailed { source: std::io::Error }, | 25 | CredentialsHelperFailed { source: std::io::Error }, |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | pub(crate) mod function { | 28 | pub(crate) mod function { |
| 29 | use crate::helper::{invoke::Error, invoke::Outcome, invoke::Result, message, Action, Context, NextAction}; | 29 | use crate::helper::{invoke::Error, invoke::Outcome, invoke::Result, Action, Context, NextAction}; |
| 30 | use bstr::ByteVec; | ||
| 31 | use std::io::Read; | 30 | use std::io::Read; |
| 32 | 31 | ||
| 33 | impl Action<'_> { | 32 | impl Action { |
| 34 | /// Send ourselves to the given `write` which is expected to be credentials-helper compatible | 33 | /// Send ourselves to the given `write` which is expected to be credentials-helper compatible |
| 35 | pub fn send(&self, mut write: impl std::io::Write) -> std::io::Result<()> { | 34 | pub fn send(&self, mut write: impl std::io::Write) -> std::io::Result<()> { |
| 36 | match self { | 35 | match self { |
| 37 | Action::Fill(url) => message::encode(url, write), | 36 | Action::Fill(ctx) => ctx.write_to(write), |
| 38 | Action::Approve(last) | Action::Reject(last) => { | 37 | Action::Approve(last) | Action::Reject(last) => { |
| 39 | write.write_all(last)?; | 38 | write.write_all(last)?; |
| 40 | write.write_all(&[b'\n']) | 39 | write.write_all(&[b'\n']) |
| @@ -47,7 +46,7 @@ pub(crate) mod function { · 6 unmodified lines | |||
| 47 | /// | 46 | /// |
| 48 | /// Usually the first call is performed with [`Action::Fill`] to obtain an identity, which subsequently can be used. | 47 | /// Usually the first call is performed with [`Action::Fill`] to obtain an identity, which subsequently can be used. |
| 49 | /// On successful usage, use [`NextAction::approve()`], otherwise [`NextAction::reject()`]. | 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 | let (stdin, stdout) = helper.start(&action)?; | 50 | let (stdin, stdout) = helper.start(&action)?; |
| 52 | action.send(stdin)?; | 51 | action.send(stdin)?; |
| 53 | let stdout = stdout | 52 | let stdout = stdout |
| @@ -67,22 +66,11 @@ pub(crate) mod function { · 13 unmodified lines | |||
| 67 | match stdout { | 66 | match stdout { |
| 68 | None => Ok(None), | 67 | None => Ok(None), |
| 69 | Some(stdout) => { | 68 | Some(stdout) => { |
| 70 | let kvs = message::decode(stdout.as_slice())?; | 69 | let ctx = Context::from_bytes(stdout.as_slice())?; |
| 71 | let find = |name: &str| { | 70 | let username = ctx.username.ok_or_else(|| Error::KeyNotFound { name: "username" })?; |
| 72 | kvs.iter() | 71 | let password = ctx.password.ok_or_else(|| Error::KeyNotFound { name: "password" })?; |
| 73 | .find(|(k, _)| k == name) | ||
| 74 | .ok_or_else(|| Error::KeyNotFound { name: name.into() }) | ||
| 75 | .map(|(_, n)| n.to_vec()) | ||
| 76 | }; | ||
| 77 | Ok(Some(Outcome { | 72 | Ok(Some(Outcome { |
| 78 | identity: git_sec::identity::Account { | 73 | identity: git_sec::identity::Account { username, password }, |
| 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 | }, | ||
| 86 | next: NextAction { | 74 | next: NextAction { |
| 87 | previous_output: stdout.into(), | 75 | previous_output: stdout.into(), |
| 88 | }, | 76 | }, |
| @@ -1,4 +1,4 @@ | |||
| 1 | use bstr::{BStr, BString}; | 1 | use bstr::BString; |
| 2 | 2 | ||
| 3 | /// The kind of helper program to use. | 3 | /// The kind of helper program to use. |
| 4 | pub enum Kind { | 4 | pub enum Kind { |
| @@ -31,16 +31,16 @@ pub mod context; · 26 unmodified lines | |||
| 31 | 31 | ||
| 32 | /// The action to perform by the credentials [helper][`crate::helper()`]. | 32 | /// The action to perform by the credentials [helper][`crate::helper()`]. |
| 33 | #[derive(Clone, Debug)] | 33 | #[derive(Clone, Debug)] |
| 34 | pub enum Action<'a> { | 34 | pub enum Action { |
| 35 | /// Provide credentials using the given repository URL (as &str) as context and pre-parsed url information as seen in [`Context`]. | 35 | /// Provide credentials using the given repository context, which must include the repository url. |
| 36 | Fill(&'a BStr), | 36 | Fill(Context), |
| 37 | /// Approve the credentials as identified by the previous input provided as `BString`, containing information from [`Context`]. | 37 | /// Approve the credentials as identified by the previous input provided as `BString`, containing information from [`Context`]. |
| 38 | Approve(BString), | 38 | Approve(BString), |
| 39 | /// Reject the credentials as identified by the previous input provided as `BString`. containing information from [`Context`]. | 39 | /// Reject the credentials as identified by the previous input provided as `BString`. containing information from [`Context`]. |
| 40 | Reject(BString), | 40 | Reject(BString), |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | impl<'a> Action<'a> { | 43 | impl Action { |
| 44 | /// Returns true if this action expects output from the helper. | 44 | /// Returns true if this action expects output from the helper. |
| 45 | pub fn expects_output(&self) -> bool { | 45 | pub fn expects_output(&self) -> bool { |
| 46 | matches!(self, Action::Fill(_)) | 46 | matches!(self, Action::Fill(_)) |
| @@ -67,11 +67,11 @@ pub struct NextAction { · 20 unmodified lines | |||
| 67 | 67 | ||
| 68 | impl NextAction { | 68 | impl NextAction { |
| 69 | /// Approve the result of the previous [Action]. | 69 | /// Approve the result of the previous [Action]. |
| 70 | pub fn approve(self) -> Action<'static> { | 70 | pub fn approve(self) -> Action { |
| 71 | Action::Approve(self.previous_output) | 71 | Action::Approve(self.previous_output) |
| 72 | } | 72 | } |
| 73 | /// Reject the result of the previous [Action]. | 73 | /// Reject the result of the previous [Action]. |
| 74 | pub fn reject(self) -> Action<'static> { | 74 | pub fn reject(self) -> Action { |
| 75 | Action::Reject(self.previous_output) | 75 | Action::Reject(self.previous_output) |
| 76 | } | 76 | } |
| 77 | } | 77 | } |
| @@ -79,45 +79,3 @@ impl NextAction { · 1 unmodified line | |||
| 79 | /// | 79 | /// |
| 80 | pub mod invoke; | 80 | pub mod invoke; |
| 81 | pub use invoke::function::invoke; | 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 | type Receive: std::io::Read; | 17 | type Receive: std::io::Read; |
| 18 | 18 | ||
| 19 | /// Start the helper and provide handles to send and receive from it. | 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 | /// Stop the helper and provide a way to determine it's successful. | 21 | /// Stop the helper and provide a way to determine it's successful. |
| 22 | fn finish(self) -> std::io::Result<()>; | 22 | fn finish(self) -> std::io::Result<()>; |
| 23 | } | 23 | } |
| @@ -38,10 +38,6 @@ pub mod helper; · 14 unmodified lines | |||
| 38 | /// Call the `git` credentials helper program performing the given `action`, without any context from git configuration. | 38 | /// Call the `git` credentials helper program performing the given `action`, without any context from git configuration. |
| 39 | /// | 39 | /// |
| 40 | /// See [`invoke()`] for a more flexible implementation. | 40 | /// See [`invoke()`] for a more flexible implementation. |
| 41 | pub fn helper(action: helper::Action<'_>) -> helper::invoke::Result { | 41 | pub fn helper(action: helper::Action) -> helper::invoke::Result { |
| 42 | helper::invoke( | 42 | helper::invoke(Program::from_kind(helper::Kind::GitCredential), action) |
| 43 | Program::from_kind(helper::Kind::GitCredential), | ||
| 44 | action, | ||
| 45 | helper::Context::default(), | ||
| 46 | ) | ||
| 47 | } | 43 | } |
| @@ -12,7 +12,7 @@ impl Helper for Program { · 11 unmodified lines | |||
| 12 | type Send = std::process::ChildStdin; | 12 | type Send = std::process::ChildStdin; |
| 13 | type Receive = std::process::ChildStdout; | 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 | match self { | 16 | match self { |
| 17 | Program::Ready(kind) => { | 17 | Program::Ready(kind) => { |
| 18 | let (mut cmd, is_custom) = match kind { | 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 | } | ||