Skip to main content

gix_credentials/protocol/
mod.rs

1use bstr::BString;
2
3use crate::helper;
4
5/// The outcome of the credentials top-level functions to obtain a complete identity.
6#[derive(Debug, Clone, Eq, PartialEq)]
7pub struct Outcome {
8    /// The identity provide by the helper.
9    pub identity: gix_sec::identity::Account,
10    /// A handle to the action to perform next in another call to [`helper::invoke()`][crate::helper::invoke()].
11    pub next: helper::NextAction,
12}
13
14/// The Result type used in credentials top-level functions to obtain a complete identity.
15pub type Result = std::result::Result<Option<Outcome>, Error>;
16
17/// The error returned top-level credential functions.
18#[derive(Debug, thiserror::Error)]
19#[expect(missing_docs)]
20pub enum Error {
21    #[error(transparent)]
22    UrlParse(#[from] gix_url::parse::Error),
23    #[error("Either 'url' field or both 'protocol' and 'host' fields must be provided")]
24    UrlMissing,
25    #[error(transparent)]
26    ContextDecode(#[from] context::decode::Error),
27    #[error(transparent)]
28    InvokeHelper(#[from] helper::Error),
29    #[error("Could not configure credential helpers")]
30    ConfigureCredentialHelpers {
31        #[source]
32        source: Box<dyn std::error::Error + Send + Sync + 'static>,
33    },
34    #[error("Could not obtain identity for context: {}", { let mut buf = Vec::<u8>::new(); context.write_to(&mut buf).ok(); String::from_utf8_lossy(&buf).into_owned() })]
35    IdentityMissing { context: Context },
36    #[error("The handler asked to stop trying to obtain credentials")]
37    Quit,
38    #[error("Couldn't obtain {prompt}")]
39    Prompt { prompt: String, source: gix_prompt::Error },
40}
41
42/// Additional context to be passed to the credentials helper.
43#[derive(Debug, Default, Clone, Eq, PartialEq)]
44pub struct Context {
45    /// Options controlling how this context is encoded and decoded.
46    pub options: ContextOptions,
47    /// The protocol over which the credential will be used (e.g., https).
48    pub protocol: Option<String>,
49    /// The remote hostname for a network credential. This includes the port number if one was specified (e.g., "example.com:8088").
50    pub host: Option<String>,
51    /// 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.
52    /// It can also be a path on the file system.
53    pub path: Option<BString>,
54    /// The credential’s username, if we already have one (e.g., from a URL, the configuration, the user, or from a previously run helper).
55    pub username: Option<String>,
56    /// The credential’s password, if we are asking it to be stored.
57    pub password: Option<String>,
58    /// An OAuth refresh token that may accompany a password. It is to be treated confidentially, just like the password.
59    pub oauth_refresh_token: Option<String>,
60    /// The expiry date of OAuth tokens as seconds from Unix epoch.
61    pub password_expiry_utc: Option<gix_date::SecondsSinceUnixEpoch>,
62    /// When this special attribute is read by git credential, the value is parsed as a URL and treated as if its constituent
63    /// parts were read (e.g., url=<https://example.com> would behave as if
64    /// protocol=https and host=example.com had been provided). This can help callers avoid parsing URLs themselves.
65    pub url: Option<BString>,
66    /// If true, the caller should stop asking for credentials immediately without calling more credential helpers in the chain.
67    pub quit: Option<bool>,
68}
69
70/// Options for encoding and decoding a [`Context`].
71#[derive(Debug, Clone, Copy, Eq, PartialEq)]
72pub struct ContextOptions {
73    /// If true, carriage returns in credential values are rejected to protect credential-protocol parsing.
74    ///
75    /// NUL bytes and newlines are always rejected.
76    pub protect_protocol: bool,
77}
78
79impl Default for ContextOptions {
80    fn default() -> Self {
81        ContextOptions { protect_protocol: true }
82    }
83}
84
85/// Convert the outcome of a helper invocation to a helper result, assuring that the identity is complete in the process.
86#[expect(
87    clippy::result_large_err,
88    reason = "will be removed once `gix-error` is used consistently"
89)]
90pub fn helper_outcome_to_result(outcome: Option<helper::Outcome>, action: helper::Action) -> Result {
91    match (action, outcome) {
92        (helper::Action::Get(ctx), None) => Err(Error::IdentityMissing {
93            context: ctx.redacted(),
94        }),
95        (helper::Action::Get(ctx), Some(mut outcome)) => match outcome.consume_identity() {
96            Some(identity) => Ok(Some(Outcome {
97                identity,
98                next: outcome.next,
99            })),
100            None => Err(if outcome.quit {
101                Error::Quit
102            } else {
103                Error::IdentityMissing {
104                    context: ctx.redacted(),
105                }
106            }),
107        },
108        (helper::Action::Store(_) | helper::Action::Erase(_), _ignore) => Ok(None),
109    }
110}
111
112///
113pub mod context;