use regex::Regex;
use std::sync::LazyLock;
static ENV_VAR_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-((?:[^}\\]|\\.)*))?}")
.expect("env-var substitution regex is a compile-time constant and must be valid")
});
static ALLOW_ALL_ENV_VARS_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?m)^allow_all_env_vars:\s*true\s*$")
.expect("allow_all_env_vars pre-scan regex is a compile-time constant and must be valid")
});
pub const ALLOWED_ENV_VARS: &[&str] = &[
"HOME",
"USER",
"USERNAME",
"LOGNAME",
"USERPROFILE", "SHELL",
"TERM",
"LANG",
"COLORTERM",
"TERM_PROGRAM",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_STATE_HOME",
"XDG_CACHE_HOME",
"XDG_RUNTIME_DIR",
"PATH",
"TMPDIR",
"TEMP",
"TMP",
"DISPLAY",
"WAYLAND_DISPLAY",
"HOSTNAME",
"HOST",
"EDITOR",
"VISUAL",
"PAGER",
"APPDATA",
"LOCALAPPDATA",
];
pub fn is_env_var_allowed(var_name: &str) -> bool {
ALLOWED_ENV_VARS.contains(&var_name)
|| var_name.starts_with("PAR_TERM_")
|| var_name.starts_with("LC_")
}
pub fn substitute_variables(input: &str) -> String {
substitute_variables_with_allowlist(input, false)
}
pub fn substitute_variables_with_allowlist(input: &str, allow_all: bool) -> String {
let escaped_placeholder = "\x00ESC_DOLLAR\x00";
let working = input.replace("$${", escaped_placeholder);
let result = ENV_VAR_PATTERN.replace_all(&working, |caps: ®ex::Captures| {
let var_name = &caps[1];
if !allow_all && !is_env_var_allowed(var_name) {
log::warn!(
"Config references non-allowlisted environment variable: ${{{var_name}}} — skipped. \
Add `allow_all_env_vars: true` to your config to allow all variables."
);
return caps[0].to_string();
}
match std::env::var(var_name) {
Ok(val) => val,
Err(_) => {
caps.get(2)
.map(|m| m.as_str().replace("\\}", "}"))
.unwrap_or_else(|| caps[0].to_string())
}
}
});
result.replace(escaped_placeholder, "${")
}
pub(crate) fn pre_scan_allow_all_env_vars(raw_yaml: &str) -> bool {
ALLOW_ALL_ENV_VARS_PATTERN.is_match(raw_yaml)
}