use super::repo_read_config_scan::{ScopeEnvironment, include_target, is_include_key, read_following};
use super::repo_read_config_text::{ConfigEntry, last_entry, parse_config_text};
use std::ffi::OsString;
use std::path::Path;
const INCLUDE_DEPTH_LIMIT: usize = 10;
const ENV_ENTRY_LIMIT: u64 = 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ListedEntry {
pub scope: &'static str,
pub entry: ConfigEntry,
}
fn add_file(path: &Path, scope: &'static str, home: Option<&Path>, depth: usize, listed: &mut Vec<ListedEntry>) -> Option<()> {
if depth > INCLUDE_DEPTH_LIMIT {
return None;
}
let Some(text) = read_following(path)? else {
return Some(());
};
for entry in parse_config_text(text.as_slice())? {
let include: Option<std::path::PathBuf> = if is_include_key(entry.key.as_str()) {
if entry.key != "include.path" {
return None;
}
Some(include_target(entry.value.as_deref()?, path, home)?)
} else {
None
};
listed.push(ListedEntry { scope, entry });
if let Some(target) = include {
add_file(target.as_path(), scope, home, depth + 1, listed)?;
}
}
return Some(());
}
fn canonical_key(key: &str) -> Option<String> {
let (section, rest) = key.split_once('.')?;
let (middle, name) = match rest.rsplit_once('.') {
Some((subsection, name)) => (Some(subsection), name),
None => (None, rest),
};
let plain = |text: &str| -> bool { return !text.is_empty() && text.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || byte == b'-'; }); };
if !plain(section) || !plain(name) || !name.as_bytes()[0].is_ascii_alphabetic() || middle.is_some_and(|text| { return text.contains('\n'); }) {
return None;
}
return Some(match middle {
Some(subsection) => format!("{}.{subsection}.{}", section.to_ascii_lowercase(), name.to_ascii_lowercase()),
None => format!("{}.{}", section.to_ascii_lowercase(), name.to_ascii_lowercase()),
});
}
fn add_environment(variables: &[(OsString, OsString)], listed: &mut Vec<ListedEntry>) -> Option<()> {
let find = |name: &str| -> Option<OsString> {
return variables.iter().rev().find(|(candidate, _)| { return candidate == name; }).map(|(_, value)| { return value.clone(); });
};
if find("GIT_CONFIG_PARAMETERS").is_some() {
return None;
}
let Some(count_text) = find("GIT_CONFIG_COUNT") else {
return Some(());
};
let count: u64 = super::child_environment::parse_config_count(count_text.as_os_str())?;
if count > ENV_ENTRY_LIMIT {
return None;
}
for index in 0..count {
let key: OsString = find(format!("GIT_CONFIG_KEY_{index}").as_str())?;
let value: OsString = find(format!("GIT_CONFIG_VALUE_{index}").as_str())?;
let entry: ConfigEntry = ConfigEntry { key: canonical_key(key.to_str()?)?, value: Some(value.as_encoded_bytes().to_vec()) };
listed.push(ListedEntry { scope: "command", entry });
}
return Some(());
}
pub fn list_config(common_dir: &Path, scope: &ScopeEnvironment, variables: &[(OsString, OsString)]) -> Option<Vec<ListedEntry>> {
if scope.unresolvable {
return None;
}
let mut listed: Vec<ListedEntry> = Vec::new();
for (label, file) in scope.labelled_files() {
add_file(file.as_path(), label, scope.home.as_deref(), 0, &mut listed)?;
}
let start: usize = listed.len();
add_file(common_dir.join("config").as_path(), "local", scope.home.as_deref(), 0, &mut listed)?;
let local: Vec<ConfigEntry> = listed[start..].iter().map(|item| { return item.entry.clone(); }).collect();
if last_entry(local.as_slice(), "extensions.worktreeconfig").is_some() {
return None;
}
add_environment(variables, &mut listed)?;
return Some(listed);
}
pub fn listing_bytes(listed: &[ListedEntry]) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
for item in listed {
out.extend_from_slice(item.scope.as_bytes());
out.push(0);
out.extend_from_slice(item.entry.key.as_bytes());
if let Some(value) = &item.entry.value {
out.push(b'\n');
out.extend_from_slice(value.as_slice());
}
out.push(0);
}
return out;
}
#[cfg(test)]
#[path = "repo_read_config_list_tests.rs"]
mod tests;