gix_credentials/helper/
cascade.rs1use crate::{
2 Program, helper,
3 helper::Cascade,
4 protocol,
5 protocol::{Context, ContextOptions},
6};
7
8impl Default for Cascade {
9 fn default() -> Self {
10 Cascade {
11 programs: Vec::new(),
12 stderr: true,
13 use_http_path: false,
14 context_options: ContextOptions::default(),
15 query_user_only: false,
16 }
17 }
18}
19
20impl Cascade {
22 pub fn platform_builtin() -> Vec<Program> {
32 if cfg!(target_os = "macos") {
33 Some("osxkeychain")
34 } else if cfg!(target_os = "linux") {
35 Some("libsecret")
36 } else if cfg!(target_os = "windows") {
37 Some("manager-core")
38 } else {
39 None
40 }
41 .map(|name| vec![Program::from_custom_definition(name)])
42 .unwrap_or_default()
43 }
44}
45
46impl Cascade {
48 pub fn extend(mut self, programs: impl IntoIterator<Item = Program>) -> Self {
50 self.programs.extend(programs);
51 self
52 }
53 pub fn use_http_path(mut self, toggle: bool) -> Self {
57 self.use_http_path = toggle;
58 self
59 }
60
61 pub fn query_user_only(mut self, toggle: bool) -> Self {
65 self.query_user_only = toggle;
66 self
67 }
68}
69
70impl Cascade {
72 #[expect(
78 clippy::result_large_err,
79 reason = "will be removed once `gix-error` is used consistently"
80 )]
81 pub fn invoke(&mut self, mut action: helper::Action, mut prompt: gix_prompt::Options) -> protocol::Result {
82 if let Some(ctx) = action.context_mut() {
83 ctx.options = self.context_options;
84 }
85 let mut url = action
86 .context_mut()
87 .map(|ctx| {
88 #[expect(
89 clippy::manual_inspect,
90 reason = "the suggested rewrite is a false positive for this mutation"
91 )] ctx.destructure_url_in_place(self.use_http_path).map(|ctx| {
93 if self.query_user_only && ctx.password.is_none() {
94 ctx.password = Some("".into());
95 }
96 ctx
97 })
98 })
99 .transpose()?
100 .and_then(|ctx| ctx.url.take());
101
102 for program in &mut self.programs {
103 program.stderr = self.stderr;
104 match helper::invoke::raw(program, &action) {
105 Ok(None) => {}
106 Ok(Some(stdout)) => {
107 let Context {
108 options: _,
109 protocol,
110 host,
111 path,
112 username,
113 password,
114 oauth_refresh_token,
115 password_expiry_utc,
116 url: ctx_url,
117 quit,
118 } = Context::from_bytes(&stdout, self.context_options)?;
119 if let Some(dst_ctx) = action.context_mut() {
120 if let Some(src) = path {
121 dst_ctx.path = Some(src);
122 }
123 if let Some(src) = password_expiry_utc {
124 dst_ctx.password_expiry_utc = Some(src);
125 }
126 for (src, dst) in [
127 (protocol, &mut dst_ctx.protocol),
128 (host, &mut dst_ctx.host),
129 (username, &mut dst_ctx.username),
130 (password, &mut dst_ctx.password),
131 (oauth_refresh_token, &mut dst_ctx.oauth_refresh_token),
132 ] {
133 if let Some(src) = src {
134 *dst = Some(src);
135 }
136 }
137 if let Some(src) = ctx_url {
138 dst_ctx.url = Some(src);
139 url = dst_ctx.destructure_url_in_place(self.use_http_path)?.url.take();
140 }
141 if dst_ctx
142 .password_expiry_utc
143 .is_some_and(|expiry_date| expiry_date < gix_date::Time::now_utc().seconds)
144 {
145 dst_ctx.password_expiry_utc = None;
146 dst_ctx.clear_secrets();
147 }
148 if dst_ctx.username.is_some() && dst_ctx.password.is_some() {
149 break;
150 }
151 if quit.unwrap_or_default() {
152 dst_ctx.quit = quit;
153 break;
154 }
155 }
156 }
157 Err(helper::Error::CredentialsHelperFailed { .. }) => continue, Err(err) if action.context().is_some() => return Err(err.into()), Err(_) => {} }
161 }
162
163 if prompt.mode != gix_prompt::Mode::Disable {
164 if let Some(ctx) = action.context_mut() {
165 ctx.url = url;
166 if ctx.username.is_none() {
167 let message = ctx.to_prompt("Username");
168 prompt.mode = gix_prompt::Mode::Visible;
169 ctx.username = gix_prompt::ask(&message, &prompt)
170 .map_err(|err| protocol::Error::Prompt {
171 prompt: message,
172 source: err,
173 })?
174 .into();
175 }
176 if ctx.password.is_none() {
177 let message = ctx.to_prompt("Password");
178 prompt.mode = gix_prompt::Mode::Hidden;
179 ctx.password = gix_prompt::ask(&message, &prompt)
180 .map_err(|err| protocol::Error::Prompt {
181 prompt: message,
182 source: err,
183 })?
184 .into();
185 }
186 }
187 }
188
189 protocol::helper_outcome_to_result(
190 action.context().map(|ctx| helper::Outcome {
191 username: ctx.username.clone(),
192 password: ctx.password.clone(),
193 oauth_refresh_token: ctx.oauth_refresh_token.clone(),
194 quit: ctx.quit.unwrap_or(false),
195 next: ctx.to_owned().into(),
196 }),
197 action,
198 )
199 }
200}