use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use super::{Config, ShellApproval};
enum Source {
#[cfg_attr(not(any(test, debug_assertions)), allow(dead_code))]
Ambient,
#[cfg_attr(all(debug_assertions, not(test)), allow(dead_code))]
Pinned(Option<PathBuf>),
}
#[cfg(any(test, feature = "test-support"))]
static TEST_PIN: Mutex<Option<PathBuf>> = Mutex::new(None);
fn source() -> Source {
#[cfg(any(test, feature = "test-support"))]
{
if let Some(home) = TEST_PIN
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
{
return Source::Pinned(Some(home));
}
}
#[cfg(any(test, debug_assertions))]
{
Source::Ambient
}
#[cfg(not(any(test, debug_assertions)))]
{
Source::Pinned(real_user_home())
}
}
#[cfg(any(test, feature = "test-support"))]
#[must_use]
pub fn pin_for_test(home: &Path) -> PinGuard {
assert!(
crate::core::data_dir::holds_test_env_lock(),
"pin_for_test requires test_env_lock()"
);
*TEST_PIN
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(home.to_path_buf());
PinGuard(())
}
#[cfg(any(test, feature = "test-support"))]
pub struct PinGuard(());
#[cfg(any(test, feature = "test-support"))]
impl Drop for PinGuard {
fn drop(&mut self) {
*TEST_PIN
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
}
}
#[must_use]
pub fn pinned() -> bool {
matches!(source(), Source::Pinned(_))
}
#[cfg(unix)]
#[must_use]
pub fn real_user_home() -> Option<PathBuf> {
use std::ffi::{CStr, OsStr};
use std::os::unix::ffi::OsStrExt;
let uid = unsafe { libc::geteuid() };
let mut buf = vec![0_u8; 4096];
loop {
let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut result: *mut libc::passwd = std::ptr::null_mut();
let rc = unsafe {
libc::getpwuid_r(
uid,
&raw mut pwd,
buf.as_mut_ptr().cast(),
buf.len(),
&raw mut result,
)
};
if rc == libc::ERANGE && buf.len() < (1 << 20) {
buf.resize(buf.len() * 2, 0);
continue;
}
if rc != 0 || result.is_null() || pwd.pw_dir.is_null() {
return None;
}
let dir = unsafe { CStr::from_ptr(pwd.pw_dir) };
let path = PathBuf::from(OsStr::from_bytes(dir.to_bytes()));
return (path.is_absolute()).then_some(path);
}
}
#[cfg(not(unix))]
#[must_use]
pub fn real_user_home() -> Option<PathBuf> {
dirs::home_dir().filter(|p| p.is_absolute())
}
fn config_dir_in(home: &Path) -> PathBuf {
let xdg_config = home.join(".config");
let xdg_data = home.join(".local").join("share");
crate::core::paths::single_dir_override_fs(home, &xdg_config, Some(&xdg_data))
.unwrap_or_else(|| xdg_config.join("lean-ctx"))
}
#[must_use]
pub fn config_path() -> Option<PathBuf> {
match source() {
Source::Ambient => Config::path(),
Source::Pinned(home) => home.map(|h| config_dir_in(&h).join("config.toml")),
}
}
#[must_use]
pub fn settings_home() -> Option<PathBuf> {
match source() {
Source::Ambient => crate::core::home::resolve_home_dir(),
Source::Pinned(home) => home,
}
}
type PinnedCache = Option<(String, Arc<Config>)>;
#[must_use]
pub fn config() -> Arc<Config> {
match source() {
Source::Ambient => Config::load_arc(),
Source::Pinned(None) => Arc::new(Config::default()),
Source::Pinned(Some(home)) => pinned_config(&home),
}
}
fn pinned_config(home: &Path) -> Arc<Config> {
static CACHE: Mutex<PinnedCache> = Mutex::new(None);
let dir = config_dir_in(home);
let global = std::fs::read_to_string(dir.join("config.toml")).ok();
let root = project_root_from_cwd();
let local = root.as_deref().and_then(|r| {
let path = Config::local_path(r);
crate::core::pathutil::may_probe_path(&path)
.then(|| std::fs::read_to_string(path).ok())
.flatten()
});
let local_hash = local.as_deref().map(crate::core::hasher::hash_str);
let trusted = match (root.as_deref(), local_hash.as_deref()) {
(Some(r), Some(hash)) => crate::core::workspace_trust::is_pinned_in_store(
&dir.join(crate::core::workspace_trust::FILE_NAME),
Path::new(r),
hash,
),
_ => false,
};
let key = format!(
"{}\0{}\0{}\0{}\0{trusted}",
dir.display(),
global
.as_deref()
.map(crate::core::hasher::hash_str)
.unwrap_or_default(),
root.as_deref().unwrap_or_default(),
local_hash.as_deref().unwrap_or_default(),
);
let mut guard = CACHE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((cached_key, cfg)) = guard.as_ref()
&& *cached_key == key
{
return Arc::clone(cfg);
}
let mut cfg = global
.as_deref()
.and_then(|raw| super::loader::parse_config_with_profile(raw, None).ok())
.unwrap_or_default();
if let Some(local) = local.as_deref() {
super::loader::without_env_profile(|| cfg.merge_local(local, trusted));
}
let cfg = Arc::new(cfg);
*guard = Some((key, Arc::clone(&cfg)));
cfg
}
fn project_root_from_cwd() -> Option<String> {
let cwd = std::env::current_dir().ok()?;
let root = cwd
.ancestors()
.find(|dir| dir.join(".git").exists())
.unwrap_or(&cwd);
Some(root.to_string_lossy().into_owned())
}
#[must_use]
pub fn env(name: &str) -> Option<String> {
match source() {
Source::Ambient => std::env::var(name).ok(),
Source::Pinned(_) => None,
}
}
fn env_flag(name: &str) -> Option<bool> {
env(name).map(|raw| {
matches!(
raw.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
}
#[must_use]
pub fn allow_inline_scripts() -> bool {
env_flag("LEAN_CTX_SHELL_ALLOW_INLINE_SCRIPTS")
.unwrap_or_else(|| config().shell_allow_inline_scripts)
}
#[must_use]
pub fn allow_writes() -> bool {
env_flag("LEAN_CTX_SHELL_ALLOW_WRITES").unwrap_or_else(|| config().shell_allow_writes)
}
#[must_use]
pub fn approval() -> ShellApproval {
match source() {
Source::Ambient => ShellApproval::effective(&config()),
Source::Pinned(_) => config().shell_approval,
}
}
#[must_use]
pub fn shell_hook_mode() -> String {
env("LEAN_CTX_SHELL_HOOK_MODE")
.filter(|v| !v.trim().is_empty())
.or_else(|| config().shell_hook_mode.clone())
.unwrap_or_default()
}
#[must_use]
pub fn cli_warn_only_requested() -> bool {
env("LEAN_CTX_ALLOWLIST_WARN_ONLY").is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn real_user_home_ignores_home_env() {
let _lock = crate::core::data_dir::test_env_lock();
let before = real_user_home();
let _home = crate::test_env::VarGuard::set("HOME", "/tmp/forged-home");
assert_eq!(real_user_home(), before);
assert_ne!(
real_user_home().as_deref(),
Some(Path::new("/tmp/forged-home"))
);
}
#[test]
fn pinned_source_ignores_env_and_relocated_config() {
let _lock = crate::core::data_dir::test_env_lock();
let real = tempfile::tempdir().unwrap();
let forged = tempfile::tempdir().unwrap();
std::fs::create_dir_all(forged.path().join(".config/lean-ctx")).unwrap();
std::fs::write(
forged.path().join(".config/lean-ctx/config.toml"),
"shell_security = \"off\"\nshell_allow_inline_scripts = true\n",
)
.unwrap();
let _pin = pin_for_test(real.path());
let _a = crate::test_env::VarGuard::set("LEAN_CTX_SHELL_SECURITY", "off");
let _b = crate::test_env::VarGuard::set("LEAN_CTX_SHELL_ALLOW_INLINE_SCRIPTS", "1");
let _c = crate::test_env::VarGuard::set("HOME", forged.path());
let _d = crate::test_env::VarGuard::set("LEAN_CTX_CONFIG_DIR", forged.path());
assert_eq!(env("LEAN_CTX_SHELL_SECURITY"), None);
assert!(!allow_inline_scripts());
assert_eq!(config().shell_security, None);
assert_eq!(
config_path(),
Some(real.path().join(".config/lean-ctx/config.toml"))
);
}
#[test]
fn pinned_source_reads_the_real_home_config() {
let _lock = crate::core::data_dir::test_env_lock();
let real = tempfile::tempdir().unwrap();
std::fs::create_dir_all(real.path().join(".config/lean-ctx")).unwrap();
std::fs::write(
real.path().join(".config/lean-ctx/config.toml"),
"shell_security = \"warn\"\n",
)
.unwrap();
let _pin = pin_for_test(real.path());
assert_eq!(config().shell_security.as_deref(), Some("warn"));
}
}