gix_credentials/program/
main.rs

1use std::ffi::OsString;
2
3use bstr::BString;
4
5/// The action passed to the credential helper implementation in [`main()`][crate::program::main()].
6#[derive(Debug, Copy, Clone)]
7pub enum Action {
8    /// Get credentials for a url.
9    Get,
10    /// Store credentials provided in the given context.
11    Store,
12    /// Erase credentials identified by the given context.
13    Erase,
14}
15
16impl TryFrom<OsString> for Action {
17    type Error = Error;
18
19    fn try_from(value: OsString) -> Result<Self, Self::Error> {
20        Ok(match value.to_str() {
21            Some("fill" | "get") => Action::Get,
22            Some("approve" | "store") => Action::Store,
23            Some("reject" | "erase") => Action::Erase,
24            _ => return Err(Error::ActionInvalid { name: value }),
25        })
26    }
27}
28
29impl Action {
30    /// Return ourselves as string representation, similar to what would be passed as argument to a credential helper.
31    pub fn as_str(&self) -> &'static str {
32        match self {
33            Action::Get => "get",
34            Action::Store => "store",
35            Action::Erase => "erase",
36        }
37    }
38}
39
40/// The error of [`main()`][crate::program::main()].
41#[derive(Debug, thiserror::Error)]
42#[allow(missing_docs)]
43pub enum Error {
44    #[error("Action named {name:?} is invalid, need 'get', 'store', 'erase' or 'fill', 'approve', 'reject'")]
45    ActionInvalid { name: OsString },
46    #[error("The first argument must be the action to perform")]
47    ActionMissing,
48    #[error(transparent)]
49    Helper {
50        source: Box<dyn std::error::Error + Send + Sync + 'static>,
51    },
52    #[error(transparent)]
53    Io(#[from] std::io::Error),
54    #[error(transparent)]
55    Context(#[from] crate::protocol::context::decode::Error),
56    #[error("Credentials for {url:?} could not be obtained")]
57    CredentialsMissing { url: BString },
58    #[error("The url wasn't provided in input - the git credentials protocol mandates this")]
59    UrlMissing,
60}
61
62pub(crate) mod function {
63    use std::ffi::OsString;
64
65    use crate::{
66        program::main::{Action, Error},
67        protocol::Context,
68    };
69
70    /// Invoke a custom credentials helper which receives program `args`, with the first argument being the
71    /// action to perform (as opposed to the program name).
72    /// Then read context information from `stdin` and if the action is `Action::Get`, then write the result to `stdout`.
73    /// `credentials` is the API version of such call, where`Ok(Some(context))` returns credentials, and `Ok(None)` indicates
74    /// no credentials could be found for `url`, which is always set when called.
75    ///
76    /// Call this function from a programs `main`, passing `std::env::args_os()`, `stdin()` and `stdout` accordingly, along with
77    /// your own helper implementation.
78    pub fn main<CredentialsFn, E>(
79        args: impl IntoIterator<Item = OsString>,
80        mut stdin: impl std::io::Read,
81        stdout: impl std::io::Write,
82        credentials: CredentialsFn,
83    ) -> Result<(), Error>
84    where
85        CredentialsFn: FnOnce(Action, Context) -> Result<Option<Context>, E>,
86        E: std::error::Error + Send + Sync + 'static,
87    {
88        let action: Action = args.into_iter().next().ok_or(Error::ActionMissing)?.try_into()?;
89        let mut buf = Vec::<u8>::with_capacity(512);
90        stdin.read_to_end(&mut buf)?;
91        let ctx = Context::from_bytes(&buf)?;
92        if ctx.url.is_none() {
93            return Err(Error::UrlMissing);
94        }
95        let res = credentials(action, ctx).map_err(|err| Error::Helper { source: Box::new(err) })?;
96        match (action, res) {
97            (Action::Get, None) => {
98                return Err(Error::CredentialsMissing {
99                    url: Context::from_bytes(&buf)?.url.expect("present and checked above"),
100                })
101            }
102            (Action::Get, Some(ctx)) => ctx.write_to(stdout)?,
103            (Action::Erase | Action::Store, None) => {}
104            (Action::Erase | Action::Store, Some(_)) => {
105                panic!("BUG: credentials helper must not return context for erase or store actions")
106            }
107        }
108        Ok(())
109    }
110}