gix_credentials/protocol/context/
mod.rs1use bstr::BString;
2
3use crate::protocol::{Context, ContextOptions};
4
5#[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 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 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 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 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 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 impl Context {
105 #[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;