1use std::path::Path;
16use std::process::Command;
17
18use crate::Error;
19
20#[derive(Debug, Default, Clone)]
21pub struct HttpOptions {
22 pub ssl_ca_info: Option<String>,
24 pub ssl_verify: Option<bool>,
27 pub ssl_cert: Option<String>,
30 pub ssl_key: Option<String>,
32 pub cookie_file: Option<String>,
37}
38
39impl HttpOptions {
40 pub fn for_url(cwd: &Path, url: &str) -> Result<Self, Error> {
44 let scoped = scoped_keys(cwd, url)?;
45 Ok(Self {
46 ssl_ca_info: scoped
47 .lookup("sslcainfo")
48 .or_else(|| get_global(cwd, "http.sslcainfo").ok().flatten()),
49 ssl_verify: scoped
50 .lookup("sslverify")
51 .or_else(|| get_global(cwd, "http.sslVerify").ok().flatten())
52 .map(|v| parse_bool(&v)),
53 ssl_cert: scoped
54 .lookup("sslcert")
55 .or_else(|| get_global(cwd, "http.sslCert").ok().flatten()),
56 ssl_key: scoped
57 .lookup("sslkey")
58 .or_else(|| get_global(cwd, "http.sslKey").ok().flatten()),
59 cookie_file: scoped
60 .lookup("cookiefile")
61 .or_else(|| get_global(cwd, "http.cookieFile").ok().flatten()),
62 })
63 }
64}
65
66struct Scoped(Vec<(String, String, String)>);
70
71impl Scoped {
72 fn lookup(&self, key: &str) -> Option<String> {
73 let key = key.to_ascii_lowercase();
74 self.0
77 .iter()
78 .find(|(_, k, _)| k.to_ascii_lowercase() == key)
79 .map(|(_, _, v)| v.clone())
80 }
81}
82
83fn scoped_keys(cwd: &Path, url: &str) -> Result<Scoped, Error> {
84 let out = Command::new("git")
85 .arg("-C")
86 .arg(cwd)
87 .args([
88 "config",
89 "--includes",
90 "--null",
91 "--get-regexp",
92 r"^http\..+\..+$",
93 ])
94 .output()?;
95 if !out.status.success() {
96 return Ok(Scoped(Vec::new()));
98 }
99 let raw = String::from_utf8_lossy(&out.stdout);
100 let mut entries: Vec<(String, String, String)> = Vec::new();
101 for record in raw.split('\0').filter(|s| !s.is_empty()) {
102 let (key_full, value) = match record.split_once('\n') {
104 Some((k, v)) => (k, v),
105 None => (record, ""),
106 };
107 let parts: Vec<&str> = key_full.splitn(2, '.').collect();
110 if parts.len() != 2 || parts[0] != "http" {
111 continue;
112 }
113 let rest = parts[1];
114 let last_dot = rest.rfind('.').unwrap_or(rest.len());
115 if last_dot == rest.len() {
116 continue;
117 }
118 let prefix = &rest[..last_dot];
119 let subkey = &rest[last_dot + 1..];
120 if url_matches(prefix, url) {
121 entries.push((prefix.to_owned(), subkey.to_owned(), value.to_owned()));
122 }
123 }
124 entries.sort_by_key(|e| std::cmp::Reverse(e.0.len()));
126 Ok(Scoped(entries))
127}
128
129fn get_global(cwd: &Path, key: &str) -> Result<Option<String>, Error> {
130 let out = Command::new("git")
131 .arg("-C")
132 .arg(cwd)
133 .args(["config", "--includes", "--get", key])
134 .output()?;
135 match out.status.code() {
136 Some(0) => Ok(Some(String::from_utf8_lossy(&out.stdout).trim().to_owned())),
137 Some(1) | Some(128) | Some(129) => Ok(None),
138 _ => Err(Error::Failed(
139 String::from_utf8_lossy(&out.stderr).trim().to_owned(),
140 )),
141 }
142}
143
144fn get_all_global(cwd: &Path, key: &str) -> Vec<String> {
148 let out = Command::new("git")
149 .arg("-C")
150 .arg(cwd)
151 .args(["config", "--includes", "--get-all", key])
152 .output();
153 match out {
154 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
155 .lines()
156 .map(str::to_owned)
157 .collect(),
158 _ => Vec::new(),
159 }
160}
161
162pub fn extra_headers_for(cwd: &Path, url: &str) -> Vec<(String, String)> {
175 let mut out: Vec<(String, String)> = Vec::new();
176 if let Ok(scoped) = scoped_keys(cwd, url) {
177 for (_prefix, subkey, value) in &scoped.0 {
178 if !subkey.eq_ignore_ascii_case("extraheader") {
179 continue;
180 }
181 if let Some(pair) = parse_header_line(value) {
182 out.push(pair);
183 }
184 }
185 }
186 for value in get_all_global(cwd, "http.extraHeader") {
187 if let Some(pair) = parse_header_line(&value) {
188 out.push(pair);
189 }
190 }
191 out
192}
193
194fn parse_header_line(s: &str) -> Option<(String, String)> {
199 let (name, value) = s.split_once(':')?;
200 let name = name.trim();
201 if name.is_empty() {
202 return None;
203 }
204 Some((name.to_owned(), value.trim().to_owned()))
205}
206
207pub fn lfs_url_bool(cwd: &Path, url: &str, subkey: &str, default: bool) -> bool {
215 let scoped = lfs_scoped_keys(cwd, url).unwrap_or(Scoped(Vec::new()));
216 if let Some(v) = scoped.lookup(subkey) {
217 return parse_bool(&v);
218 }
219 let global_key = format!("lfs.{subkey}");
220 match get_global(cwd, &global_key) {
221 Ok(Some(v)) => parse_bool(&v),
222 _ => default,
223 }
224}
225
226fn lfs_scoped_keys(cwd: &Path, url: &str) -> Result<Scoped, Error> {
229 let out = Command::new("git")
230 .arg("-C")
231 .arg(cwd)
232 .args([
233 "config",
234 "--includes",
235 "--null",
236 "--get-regexp",
237 r"^lfs\..+\..+$",
238 ])
239 .output()?;
240 if !out.status.success() {
241 return Ok(Scoped(Vec::new()));
242 }
243 let raw = String::from_utf8_lossy(&out.stdout);
244 let mut entries: Vec<(String, String, String)> = Vec::new();
245 for record in raw.split('\0').filter(|s| !s.is_empty()) {
246 let (key_full, value) = match record.split_once('\n') {
247 Some((k, v)) => (k, v),
248 None => (record, ""),
249 };
250 let parts: Vec<&str> = key_full.splitn(2, '.').collect();
251 if parts.len() != 2 || parts[0] != "lfs" {
252 continue;
253 }
254 let rest = parts[1];
255 let Some(last_dot) = rest.rfind('.') else {
256 continue;
257 };
258 let prefix = &rest[..last_dot];
259 let subkey = &rest[last_dot + 1..];
260 if url_matches(prefix, url) {
261 entries.push((prefix.to_owned(), subkey.to_owned(), value.to_owned()));
262 }
263 }
264 entries.sort_by_key(|e| std::cmp::Reverse(e.0.len()));
265 Ok(Scoped(entries))
266}
267
268fn url_matches(prefix: &str, url: &str) -> bool {
274 let p = prefix.trim_end_matches('/').to_ascii_lowercase();
275 let u = url.trim_end_matches('/').to_ascii_lowercase();
276 u == p || u.starts_with(&format!("{p}/"))
277}
278
279fn parse_bool(s: &str) -> bool {
280 matches!(
281 s.trim().to_ascii_lowercase().as_str(),
282 "true" | "1" | "yes" | "on"
283 )
284}