use std::path::PathBuf;
use crate::Config;
use super::Cli;
pub(super) const MAX_FILE_SIZE_CEILING: u64 = 536_870_912;
pub(super) fn resolve_config(cli: &Cli) -> Config {
let repo_root = cli
.repo_root
.clone()
.or_else(detect_repo_root)
.unwrap_or_else(|| PathBuf::from("."));
let index_dir = {
let raw = cli
.index_dir
.clone()
.unwrap_or_else(|| repo_root.join(".syntext"));
if let Err(msg) = validate_index_dir(&raw) {
eprintln!("{msg}");
std::process::exit(2);
}
raw
};
let max_file_size = parse_max_file_size();
let verify_on_open = matches!(
std::env::var("SYNTEXT_VERIFY_ON_OPEN").ok().as_deref(),
Some("1") | Some("true")
);
let auto_update = !cli.no_update
&& !matches!(
std::env::var("SYNTEXT_NO_AUTO_UPDATE").ok().as_deref(),
Some("1") | Some("true")
);
let auto_update_async_catchup =
resolve_auto_update_async_catchup(std::env::var("SYNTEXT_NO_ASYNC_UPDATE").ok().as_deref());
Config {
max_file_size,
max_segments: 10,
index_dir,
repo_root,
verbose: false,
strict_permissions: true,
verify_on_open,
recalibrate: false,
auto_update,
auto_update_max_files: std::env::var("SYNTEXT_AUTO_UPDATE_MAX_FILES")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(200),
auto_update_budget_ms: std::env::var("SYNTEXT_AUTO_UPDATE_BUDGET_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(150),
auto_update_async_catchup,
#[cfg(feature = "rayon")]
thread_pool: None,
}
}
fn parse_max_file_size() -> u64 {
clamp_max_file_size(
std::env::var("SYNTEXT_MAX_FILE_SIZE")
.ok()
.and_then(|v| v.parse::<u64>().ok()),
)
}
pub(super) fn clamp_max_file_size(raw: Option<u64>) -> u64 {
raw.unwrap_or(10 * 1024 * 1024).min(MAX_FILE_SIZE_CEILING)
}
pub(super) fn resolve_auto_update_async_catchup(raw: Option<&str>) -> bool {
!matches!(raw, Some("1") | Some("true"))
}
pub(super) fn detect_repo_root() -> Option<PathBuf> {
let mut dir = std::env::current_dir().ok()?;
loop {
if dir.join(".git").exists() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
#[cfg(unix)]
fn validate_index_dir(index_dir: &std::path::Path) -> Result<(), String> {
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let resolved = if index_dir.is_absolute() {
index_dir.to_path_buf()
} else {
cwd.join(index_dir)
};
let mut ancestor = resolved.clone();
let mut popped = Vec::new();
while !ancestor.exists() {
if let Some(p) = ancestor.parent() {
if let Some(file_name) = ancestor.file_name() {
popped.push(file_name.to_os_string());
}
ancestor = p.to_path_buf();
} else {
break;
}
}
let mut canonical = std::fs::canonicalize(&ancestor).unwrap_or(ancestor);
for file_name in popped.into_iter().rev() {
canonical.push(file_name);
}
const SENSITIVE_PREFIXES: &[&str] = &[
"/etc",
"/usr",
"/bin",
"/sbin",
"/lib",
"/lib64",
"/sys",
"/proc",
"/dev",
"/boot",
"/root",
"/System",
"/Library",
"/private/etc",
"/private/var/root",
];
let dir_str = canonical.to_string_lossy();
if let Some(matched) = overlaps_sensitive_prefix(&dir_str, SENSITIVE_PREFIXES, '/') {
return Err(format!(
"st: refusing --index-dir '{}': overlaps system path '{matched}'; \
use a path under the repository or a user-owned directory",
index_dir.display(),
));
}
Ok(())
}
#[cfg(not(unix))]
fn validate_index_dir(index_dir: &std::path::Path) -> Result<(), String> {
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let resolved = if index_dir.is_absolute() {
index_dir.to_path_buf()
} else {
cwd.join(index_dir)
};
let mut ancestor = resolved.clone();
let mut popped = Vec::new();
while !ancestor.exists() {
if let Some(p) = ancestor.parent() {
if let Some(file_name) = ancestor.file_name() {
popped.push(file_name.to_os_string());
}
ancestor = p.to_path_buf();
} else {
break;
}
}
let mut canonical = std::fs::canonicalize(&ancestor).unwrap_or(ancestor);
for file_name in popped.into_iter().rev() {
canonical.push(file_name);
}
let mut sensitive: Vec<String> = Vec::new();
for var in [
"SYSTEMROOT",
"PROGRAMFILES",
"PROGRAMFILES(X86)",
"PROGRAMDATA",
] {
if let Some(val) = std::env::var_os(var) {
let lower = val.to_string_lossy().to_lowercase().replace('/', "\\");
if !sensitive.contains(&lower) {
sensitive.push(lower);
}
}
}
for fb in [
"c:\\windows",
"c:\\program files",
"c:\\program files (x86)",
"c:\\programdata",
] {
let s = fb.to_string();
if !sensitive.contains(&s) {
sensitive.push(s);
}
}
let mut dir_lower = canonical
.to_string_lossy()
.to_lowercase()
.replace('/', "\\");
if dir_lower.starts_with("\\\\?\\") {
dir_lower = dir_lower[4..].to_string();
}
let prefixes: Vec<&str> = sensitive.iter().map(|s| s.as_str()).collect();
if let Some(matched) = overlaps_sensitive_prefix(&dir_lower, &prefixes, '\\') {
return Err(format!(
"st: refusing --index-dir '{}': overlaps system path '{matched}'; \
use a path under the repository or a user-owned directory",
index_dir.display(),
));
}
Ok(())
}
pub(super) fn overlaps_sensitive_prefix<'a>(
dir: &str,
prefixes: &[&'a str],
sep: char,
) -> Option<&'a str> {
for &prefix in prefixes {
if dir == prefix || dir.starts_with(&format!("{prefix}{sep}")) {
return Some(prefix);
}
}
None
}
#[cfg(test)]
mod tests {
use super::resolve_config;
use crate::cli::Cli;
use clap::Parser;
#[test]
fn auto_update_enabled_by_default() {
let cli = Cli::try_parse_from(["st", "pattern"]).expect("parse failed");
let config = resolve_config(&cli);
assert!(config.auto_update, "auto_update should be true by default");
}
#[test]
fn no_update_flag_disables_auto_update() {
let cli = Cli::try_parse_from(["st", "--no-update", "pattern"]).expect("parse failed");
let config = resolve_config(&cli);
assert!(
!config.auto_update,
"--no-update should set auto_update=false"
);
}
#[test]
fn auto_update_defaults_are_sensible() {
let cli = Cli::try_parse_from(["st", "pattern"]).expect("parse failed");
let config = resolve_config(&cli);
assert!(config.auto_update_max_files > 0);
assert!(config.auto_update_budget_ms > 0);
}
#[test]
fn async_catchup_enabled_by_default() {
use super::resolve_auto_update_async_catchup;
assert!(resolve_auto_update_async_catchup(None));
}
#[test]
fn async_catchup_disabled_by_env() {
use super::resolve_auto_update_async_catchup;
assert!(!resolve_auto_update_async_catchup(Some("1")));
assert!(!resolve_auto_update_async_catchup(Some("true")));
}
#[test]
fn async_catchup_enabled_for_unrecognized_values() {
use super::resolve_auto_update_async_catchup;
assert!(resolve_auto_update_async_catchup(Some("0")));
assert!(resolve_auto_update_async_catchup(Some("")));
}
#[test]
fn config_honors_no_async_update_env_absent() {
if std::env::var("SYNTEXT_NO_ASYNC_UPDATE").is_err() {
let cli = Cli::try_parse_from(["st", "pattern"]).expect("parse failed");
let config = resolve_config(&cli);
assert!(config.auto_update_async_catchup);
}
}
}