use crate::diagnostic_log::debug;
use crate::transaction_git::{GitContext, GitRequest, run_git};
use crate::config_fsync::FsyncComponents;
use std::collections::BTreeMap;
use std::path::Path;
const DECIMAL_DIGITS_LIMIT: usize = 9;
pub const MAINTENANCE_KEYS: [&str; 4] = ["maintenance.auto", "maintenance.autodetach", "gc.auto", "gc.autodetach"];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigEntry {
pub scope: String,
pub key: String,
pub value: Option<Vec<u8>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Lookup<Value> {
Unset,
Found(Value),
Unhandled,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigSnapshot {
entries: Vec<ConfigEntry>,
without_global_options: bool,
}
fn plain_decimal(text: &str) -> Option<u32> {
if text == "0" {
return Some(0);
}
let first: u8 = *text.as_bytes().first()?;
if !(b'1'..=b'9').contains(&first) || text.len() > DECIMAL_DIGITS_LIMIT {
return None;
}
if !text.bytes().all(|byte| { return byte.is_ascii_digit(); }) {
return None;
}
return text.parse::<u32>().ok();
}
fn boolean_word(text: &str) -> Option<bool> {
for word in ["true", "yes", "on"] {
if text.eq_ignore_ascii_case(word) {
return Some(true);
}
}
for word in ["false", "no", "off"] {
if text.eq_ignore_ascii_case(word) {
return Some(false);
}
}
return None;
}
pub fn canonical_bool(value: Option<&[u8]>) -> Option<bool> {
let Some(bytes) = value else {
return Some(true);
};
let text: &str = std::str::from_utf8(bytes).ok()?;
if text.is_empty() {
return Some(false);
}
if let Some(word) = boolean_word(text) {
return Some(word);
}
return plain_decimal(text).map(|number| { return number != 0; });
}
pub fn canonical_bool_or_int(value: Option<&[u8]>) -> Option<String> {
let Some(bytes) = value else {
return Some(String::from("true"));
};
let text: &str = std::str::from_utf8(bytes).ok()?;
if text.is_empty() {
return Some(String::from("false"));
}
if let Some(word) = boolean_word(text) {
return Some(String::from(if word { "true" } else { "false" }));
}
return plain_decimal(text).map(|number| { return number.to_string(); });
}
pub fn plain_path(value: Option<&[u8]>) -> Option<String> {
let text: &str = std::str::from_utf8(value?).ok()?;
if text.is_empty() || text.starts_with('~') || text.starts_with("%(") || text.starts_with(":(") {
return None;
}
return Some(String::from(text));
}
fn split_record(record: &[u8]) -> (&[u8], Option<&[u8]>) {
return match record.iter().position(|byte| { return *byte == b'\n'; }) {
Some(newline) => (&record[..newline], Some(&record[newline + 1..])),
None => (record, None),
};
}
impl ConfigSnapshot {
pub fn parse(listing: &[u8], without_global_options: bool) -> Option<Self> {
let mut entries: Vec<ConfigEntry> = Vec::new();
if listing.is_empty() {
return Some(Self { entries, without_global_options });
}
let body: &[u8] = listing.strip_suffix(&[0])?;
let mut fields = body.split(|byte| { return *byte == 0; });
while let Some(scope) = fields.next() {
let record: &[u8] = fields.next()?;
let (key, value): (&[u8], Option<&[u8]>) = split_record(record);
let key_text: &str = std::str::from_utf8(key).ok()?;
if key_text.is_empty() {
return None;
}
entries.push(ConfigEntry {
scope: String::from(std::str::from_utf8(scope).ok()?),
key: String::from(key_text),
value: value.map(<[u8]>::to_vec),
});
}
return Some(Self { entries, without_global_options });
}
pub fn fsync_components(&self) -> Option<FsyncComponents> {
let Some(entry) = self.entries.iter().rfind(|entry| { return entry.key == "core.fsync"; }) else { return Some(FsyncComponents::default_set()) };
let value: &str = std::str::from_utf8(entry.value.as_deref()?).ok()?;
return Some(FsyncComponents::parse(value));
}
pub fn hardens_index(&self) -> Option<bool> {
return self.fsync_components().map(FsyncComponents::names_index);
}
pub fn import_fsync_value(&self) -> Option<String> {
let set: FsyncComponents = self.fsync_components()?;
let legacy = self.entries.iter().rfind(|entry| { return entry.key == "core.fsyncobjectfiles"; });
let legacy_flush: bool = match legacy {
None => false,
Some(entry) => canonical_bool(entry.value.as_deref())?,
};
if set.names_loose_object() || legacy_flush {
return if set.names_pack_and_metadata() { None } else { Some(set.with_packs().value()) };
}
if !set.names_pack() {
return None;
}
return Some(set.without_packs().value());
}
pub fn entries(&self) -> &[ConfigEntry] {
return self.entries.as_slice();
}
pub fn without_global_options(&self) -> bool {
return self.without_global_options;
}
fn last_converted<Value>(
&self,
key: &str,
convert: impl Fn(Option<&[u8]>) -> Option<Value>,
) -> Lookup<Value> {
let mut last: Lookup<Value> = Lookup::Unset;
for entry in self.entries.iter().filter(|candidate| { return candidate.key == key; }) {
match convert(entry.value.as_deref()) {
Some(converted) => last = Lookup::Found(converted),
None => return Lookup::Unhandled,
}
}
return last;
}
pub fn path_value(&self, key: &str) -> Lookup<String> {
return self.last_converted(key, plain_path);
}
pub fn bool_value(&self, key: &str) -> Lookup<bool> {
return self.last_converted(key, canonical_bool);
}
pub fn maintenance_records(&self) -> Option<BTreeMap<String, String>> {
let mut records: BTreeMap<String, String> = BTreeMap::new();
for key in MAINTENANCE_KEYS {
match self.last_converted(key, canonical_bool_or_int) {
Lookup::Unset => {}
Lookup::Found(text) => {
records.insert(String::from(key), text);
}
Lookup::Unhandled => return None,
}
}
return Some(records);
}
pub fn proves_files_ref_storage(&self) -> bool {
return self.without_global_options
&& !self.entries.iter().any(|entry| { return entry.key == "extensions.refstorage"; });
}
}
pub fn read_config_snapshot(context: &GitContext, cwd: &Path) -> Option<ConfigSnapshot> {
let request: GitRequest = GitRequest::new(cwd, &["config", "--list", "-z", "--show-scope"]);
let output = match run_git(context, &request) {
Ok(finished) => finished,
Err(error) => {
debug("readConfigSnapshot", format!("git config --list could not start: {error}").as_str());
return None;
}
};
if !output.succeeded() {
debug(
"readConfigSnapshot",
format!("git config --list failed: {}", output.error_text()).as_str(),
);
return None;
}
let parsed: Option<ConfigSnapshot> =
ConfigSnapshot::parse(output.stdout.as_slice(), context.global_prefix.is_empty());
if parsed.is_none() {
debug("readConfigSnapshot", "the configuration listing has an unexpected shape");
}
return parsed;
}
#[cfg(test)]
#[path = "config_snapshot_tests.rs"]
mod tests;