pub(crate) const CONFIG_KEYS: [&str; 7] = ["policy", "baseline", "strict", "no-ambient", "closed-world", "taint", "deps"];
pub(crate) const CONFIG_KEYS_IMPLEMENTED: [&str; 3] = ["policy", "baseline", "deps"];
pub(crate) fn load_candor_config(dir: &str) -> std::collections::HashMap<String, String> {
let file: Option<std::path::PathBuf> = match std::env::var("CANDOR_CONFIG") {
Ok(p) => {
let pb = std::path::PathBuf::from(&p);
if !pb.is_file() {
eprintln!("candor-scan: CANDOR_CONFIG set but {p} is not a readable file — failing (exit 2)");
std::process::exit(2);
}
Some(pb)
}
Err(_) => {
let start = std::fs::canonicalize(dir).unwrap_or_else(|_| std::path::PathBuf::from(dir));
let mut cur = if start.is_dir() { Some(start.as_path()) } else { start.parent() };
let mut found = None;
while let Some(d) = cur {
let cand = d.join(".candor/config");
if cand.exists() {
found = Some(cand);
break;
}
cur = d.parent();
}
found.or_else(|| {
let cwd = std::path::PathBuf::from(".candor/config");
if cwd.exists() { Some(cwd) } else { None }
})
}
};
let Some(file) = file else { return std::collections::HashMap::new() };
let text = match std::fs::read_to_string(&file) {
Ok(t) => t,
Err(e) => {
eprintln!("candor-scan: config {} exists but could not be read ({e}) — failing (exit 2)", file.display());
std::process::exit(2);
}
};
let mut cfg = std::collections::HashMap::new();
for raw in text.lines() {
let line = raw.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let mut it = line.splitn(2, char::is_whitespace);
let key = it.next().unwrap_or("").to_ascii_lowercase();
let val = it.next().unwrap_or("").trim().to_string();
if !CONFIG_KEYS.contains(&key.as_str()) {
eprintln!("candor-scan: ignoring unknown config key '{key}' in {}", file.display());
continue;
}
if !CONFIG_KEYS_IMPLEMENTED.contains(&key.as_str()) {
eprintln!(
"candor-scan: config key '{key}' is recognized by the candor family but not \
implemented by candor-scan — that gate/mode is NOT active on this scan \
(the nightly lint / another engine enforces it)"
);
continue;
}
cfg.insert(key, val);
}
let base = {
let parent = file.parent().map(std::path::Path::to_path_buf).unwrap_or_default();
if parent.file_name().and_then(|n| n.to_str()) == Some(".candor") {
parent.parent().map(std::path::Path::to_path_buf).unwrap_or(parent)
} else {
parent
}
};
let resolve = |v: &str| -> String {
if v.is_empty() || std::path::Path::new(v).is_absolute() {
v.to_string()
} else {
base.join(v).to_string_lossy().into_owned()
}
};
if let Some(p) = cfg.get_mut("policy") {
*p = resolve(p);
}
if let Some(b) = cfg.get_mut("baseline") {
*b = resolve(b);
}
if let Some(d) = cfg.get_mut("deps") {
*d = d.split(':').map(&resolve).collect::<Vec<_>>().join(":");
}
cfg
}