Skip to main content

gix_credentials/protocol/context/
mod.rs

1use bstr::BString;
2
3use crate::protocol::{Context, ContextOptions};
4
5/// Indicates key or values contain errors that can't be encoded.
6#[derive(Debug, thiserror::Error)]
7#[expect(missing_docs)]
8pub enum Error {
9    #[error("{key:?}={value:?} must not contain null bytes or newlines neither in key nor in value.")]
10    Encoding { key: String, value: BString },
11}
12
13impl Context {
14    /// Create a context containing `url`, encoded and decoded according to `options`.
15    pub fn from_url(url: impl Into<BString>, options: ContextOptions) -> Self {
16        Context {
17            options,
18            url: Some(url.into()),
19            ..Default::default()
20        }
21    }
22}
23
24mod access {
25    use bstr::BString;
26
27    use crate::protocol::Context;
28
29    impl Context {
30        /// Clear all fields that are considered secret.
31        pub fn clear_secrets(&mut self) {
32            let Context {
33                options: _,
34                protocol: _,
35                host: _,
36                path: _,
37                username: _,
38                password,
39                oauth_refresh_token,
40                password_expiry_utc: _,
41                url: _,
42                quit: _,
43            } = self;
44
45            *password = None;
46            *oauth_refresh_token = None;
47        }
48        /// Replace existing secrets with the word `<redacted>`.
49        pub fn redacted(mut self) -> Self {
50            let Context {
51                options: _,
52                protocol: _,
53                host: _,
54                path: _,
55                username: _,
56                password,
57                oauth_refresh_token,
58                password_expiry_utc: _,
59                url: _,
60                quit: _,
61            } = &mut self;
62            for secret in [password, oauth_refresh_token].into_iter().flatten() {
63                *secret = "<redacted>".into();
64            }
65            self
66        }
67
68        /// Convert all relevant fields into a URL for consumption.
69        pub fn to_url(&self) -> Option<BString> {
70            use bstr::{ByteSlice, ByteVec};
71            let mut buf: BString = self.protocol.clone()?.into();
72            buf.push_str(b"://");
73            if let Some(user) = &self.username {
74                buf.push_str(user);
75                buf.push(b'@');
76            }
77            if let Some(host) = &self.host {
78                buf.push_str(host);
79            }
80            if let Some(path) = &self.path {
81                if !path.starts_with_str("/") {
82                    buf.push(b'/');
83                }
84                buf.push_str(path);
85            }
86            buf.into()
87        }
88        /// Compute a prompt to obtain the given value.
89        pub fn to_prompt(&self, field: &str) -> String {
90            match self.to_url() {
91                Some(url) => format!("{field} for {url}: "),
92                None => format!("{field}: "),
93            }
94        }
95    }
96}
97
98mod mutate {
99    use bstr::ByteSlice;
100
101    use crate::{protocol, protocol::Context};
102
103    /// In-place mutation
104    impl Context {
105        /// Destructure the url at our `url` field into parts like protocol, host, username and path and store
106        /// them in our respective fields. If `use_http_path` is set, http paths are significant even though
107        /// normally this isn't the case.
108        #[expect(
109            clippy::result_large_err,
110            reason = "will be removed once `gix-error` is used consistently"
111        )]
112        pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> Result<&mut Self, protocol::Error> {
113            if self.url.is_none() {
114                self.url = Some(self.to_url().ok_or(protocol::Error::UrlMissing)?);
115            }
116
117            let url = gix_url::parse(self.url.as_ref().expect("URL is present after check above"))?;
118            self.protocol = Some(url.scheme.as_str().into());
119            self.username = url.user().map(ToOwned::to_owned);
120            self.password = url.password().map(ToOwned::to_owned);
121            self.host = url.host().map(ToOwned::to_owned).map(|mut host| {
122                let port = url.port.filter(|port| {
123                    url.scheme
124                        .default_port()
125                        .is_none_or(|default_port| *port != default_port)
126                });
127                if let Some(port) = port {
128                    use std::fmt::Write;
129                    write!(host, ":{port}").expect("infallible");
130                }
131                host
132            });
133            if !matches!(url.scheme, gix_url::Scheme::Http | gix_url::Scheme::Https) || use_http_path {
134                let path = url.path.trim_with(|b| b == '/');
135                self.path = (!path.is_empty()).then(|| path.into());
136            }
137            Ok(self)
138        }
139    }
140}
141
142mod serde;
143pub use self::serde::decode;