1use std::path::Path;
15use std::process::Command;
16
17use crate::Error;
18
19#[derive(Debug, Default, Clone)]
20pub struct HttpOptions {
21 pub ssl_ca_info: Option<String>,
23 pub ssl_verify: Option<bool>,
26 pub ssl_cert: Option<String>,
29 pub ssl_key: Option<String>,
31}
32
33impl HttpOptions {
34 pub fn for_url(cwd: &Path, url: &str) -> Result<Self, Error> {
38 let scoped = scoped_keys(cwd, url)?;
39 Ok(Self {
40 ssl_ca_info: scoped
41 .lookup("sslcainfo")
42 .or_else(|| get_global(cwd, "http.sslcainfo").ok().flatten()),
43 ssl_verify: scoped
44 .lookup("sslverify")
45 .or_else(|| get_global(cwd, "http.sslVerify").ok().flatten())
46 .map(|v| parse_bool(&v)),
47 ssl_cert: scoped
48 .lookup("sslcert")
49 .or_else(|| get_global(cwd, "http.sslCert").ok().flatten()),
50 ssl_key: scoped
51 .lookup("sslkey")
52 .or_else(|| get_global(cwd, "http.sslKey").ok().flatten()),
53 })
54 }
55}
56
57struct Scoped(Vec<(String, String, String)>);
61
62impl Scoped {
63 fn lookup(&self, key: &str) -> Option<String> {
64 let key = key.to_ascii_lowercase();
65 self.0
68 .iter()
69 .find(|(_, k, _)| k.to_ascii_lowercase() == key)
70 .map(|(_, _, v)| v.clone())
71 }
72}
73
74fn scoped_keys(cwd: &Path, url: &str) -> Result<Scoped, Error> {
75 let out = Command::new("git")
76 .arg("-C")
77 .arg(cwd)
78 .args([
79 "config",
80 "--includes",
81 "--null",
82 "--get-regexp",
83 r"^http\..+\..+$",
84 ])
85 .output()?;
86 if !out.status.success() {
87 return Ok(Scoped(Vec::new()));
89 }
90 let raw = String::from_utf8_lossy(&out.stdout);
91 let mut entries: Vec<(String, String, String)> = Vec::new();
92 for record in raw.split('\0').filter(|s| !s.is_empty()) {
93 let (key_full, value) = match record.split_once('\n') {
95 Some((k, v)) => (k, v),
96 None => (record, ""),
97 };
98 let parts: Vec<&str> = key_full.splitn(2, '.').collect();
101 if parts.len() != 2 || parts[0] != "http" {
102 continue;
103 }
104 let rest = parts[1];
105 let last_dot = rest.rfind('.').unwrap_or(rest.len());
106 if last_dot == rest.len() {
107 continue;
108 }
109 let prefix = &rest[..last_dot];
110 let subkey = &rest[last_dot + 1..];
111 if url_matches(prefix, url) {
112 entries.push((prefix.to_owned(), subkey.to_owned(), value.to_owned()));
113 }
114 }
115 entries.sort_by_key(|e| std::cmp::Reverse(e.0.len()));
117 Ok(Scoped(entries))
118}
119
120fn get_global(cwd: &Path, key: &str) -> Result<Option<String>, Error> {
121 let out = Command::new("git")
122 .arg("-C")
123 .arg(cwd)
124 .args(["config", "--includes", "--get", key])
125 .output()?;
126 match out.status.code() {
127 Some(0) => Ok(Some(String::from_utf8_lossy(&out.stdout).trim().to_owned())),
128 Some(1) | Some(128) | Some(129) => Ok(None),
129 _ => Err(Error::Failed(
130 String::from_utf8_lossy(&out.stderr).trim().to_owned(),
131 )),
132 }
133}
134
135fn get_all_global(cwd: &Path, key: &str) -> Vec<String> {
139 let out = Command::new("git")
140 .arg("-C")
141 .arg(cwd)
142 .args(["config", "--includes", "--get-all", key])
143 .output();
144 match out {
145 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
146 .lines()
147 .map(str::to_owned)
148 .collect(),
149 _ => Vec::new(),
150 }
151}
152
153pub fn extra_headers_for(cwd: &Path, url: &str) -> Vec<(String, String)> {
166 let mut out: Vec<(String, String)> = Vec::new();
167 if let Ok(scoped) = scoped_keys(cwd, url) {
168 for (_prefix, subkey, value) in &scoped.0 {
169 if !subkey.eq_ignore_ascii_case("extraheader") {
170 continue;
171 }
172 if let Some(pair) = parse_header_line(value) {
173 out.push(pair);
174 }
175 }
176 }
177 for value in get_all_global(cwd, "http.extraHeader") {
178 if let Some(pair) = parse_header_line(&value) {
179 out.push(pair);
180 }
181 }
182 out
183}
184
185fn parse_header_line(s: &str) -> Option<(String, String)> {
190 let (name, value) = s.split_once(':')?;
191 let name = name.trim();
192 if name.is_empty() {
193 return None;
194 }
195 Some((name.to_owned(), value.trim().to_owned()))
196}
197
198pub fn lfs_url_bool(cwd: &Path, url: &str, subkey: &str, default: bool) -> bool {
206 let scoped = lfs_scoped_keys(cwd, url).unwrap_or(Scoped(Vec::new()));
207 if let Some(v) = scoped.lookup(subkey) {
208 return parse_bool(&v);
209 }
210 let global_key = format!("lfs.{subkey}");
211 match get_global(cwd, &global_key) {
212 Ok(Some(v)) => parse_bool(&v),
213 _ => default,
214 }
215}
216
217fn lfs_scoped_keys(cwd: &Path, url: &str) -> Result<Scoped, Error> {
220 let out = Command::new("git")
221 .arg("-C")
222 .arg(cwd)
223 .args([
224 "config",
225 "--includes",
226 "--null",
227 "--get-regexp",
228 r"^lfs\..+\..+$",
229 ])
230 .output()?;
231 if !out.status.success() {
232 return Ok(Scoped(Vec::new()));
233 }
234 let raw = String::from_utf8_lossy(&out.stdout);
235 let mut entries: Vec<(String, String, String)> = Vec::new();
236 for record in raw.split('\0').filter(|s| !s.is_empty()) {
237 let (key_full, value) = match record.split_once('\n') {
238 Some((k, v)) => (k, v),
239 None => (record, ""),
240 };
241 let parts: Vec<&str> = key_full.splitn(2, '.').collect();
242 if parts.len() != 2 || parts[0] != "lfs" {
243 continue;
244 }
245 let rest = parts[1];
246 let Some(last_dot) = rest.rfind('.') else {
247 continue;
248 };
249 let prefix = &rest[..last_dot];
250 let subkey = &rest[last_dot + 1..];
251 if url_matches(prefix, url) {
252 entries.push((prefix.to_owned(), subkey.to_owned(), value.to_owned()));
253 }
254 }
255 entries.sort_by_key(|e| std::cmp::Reverse(e.0.len()));
256 Ok(Scoped(entries))
257}
258
259fn url_matches(prefix: &str, url: &str) -> bool {
265 let p = prefix.trim_end_matches('/').to_ascii_lowercase();
266 let u = url.trim_end_matches('/').to_ascii_lowercase();
267 u == p || u.starts_with(&format!("{p}/"))
268}
269
270fn parse_bool(s: &str) -> bool {
271 matches!(
272 s.trim().to_ascii_lowercase().as_str(),
273 "true" | "1" | "yes" | "on"
274 )
275}