use crate::security::is_credential_like_key;
use std::{
collections::BTreeMap,
ffi::{OsStr, OsString},
process::Command,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SubprocessEnvProfile {
Shell,
Lsp,
McpStdio,
}
#[derive(Debug, Clone, Copy)]
struct ProfileRules {
exact: &'static [&'static str],
locale: &'static [&'static str],
include_windows_startup: bool,
}
const EMPTY: &[&str] = &[];
const LOCALE_VARIABLES: &[&str] = &[
"LC_CTYPE",
"LC_NUMERIC",
"LC_TIME",
"LC_COLLATE",
"LC_MONETARY",
"LC_MESSAGES",
"LC_PAPER",
"LC_NAME",
"LC_ADDRESS",
"LC_TELEPHONE",
"LC_MEASUREMENT",
"LC_IDENTIFICATION",
"LC_ALL",
];
const SHELL_VARIABLES: &[&str] = &["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "LANG"];
const PATH_ONLY: &[&str] = &["PATH"];
#[cfg(windows)]
fn matches_environment_name(actual: &str, expected: &str) -> bool {
actual.eq_ignore_ascii_case(expected)
}
#[cfg(not(windows))]
fn matches_environment_name(actual: &str, expected: &str) -> bool {
actual == expected
}
impl SubprocessEnvProfile {
fn rules(self) -> ProfileRules {
match self {
Self::Shell => ProfileRules {
exact: SHELL_VARIABLES,
locale: LOCALE_VARIABLES,
include_windows_startup: true,
},
Self::Lsp => ProfileRules {
exact: PATH_ONLY,
locale: EMPTY,
include_windows_startup: false,
},
Self::McpStdio => ProfileRules {
exact: PATH_ONLY,
locale: EMPTY,
include_windows_startup: true,
},
}
}
pub(crate) fn allows_ambient(self, key: &OsStr) -> bool {
let Some(key) = key.to_str() else {
return false;
};
if is_credential_like_key(key) {
return false;
}
let rules = self.rules();
(rules.include_windows_startup && is_windows_startup_variable(key))
|| rules
.exact
.iter()
.any(|expected| matches_environment_name(key, expected))
|| rules
.locale
.iter()
.any(|expected| matches_environment_name(key, expected))
}
}
pub(crate) fn apply_profile(command: &mut Command, profile: SubprocessEnvProfile) {
apply_profile_from(command, profile, std::env::vars_os());
}
pub(crate) fn apply_configured_profile(
command: &mut Command,
profile: SubprocessEnvProfile,
configured: &BTreeMap<String, String>,
) {
apply_configured_profile_from(command, profile, std::env::vars_os(), configured);
}
fn apply_profile_from(
command: &mut Command,
profile: SubprocessEnvProfile,
environment: impl IntoIterator<Item = (OsString, OsString)>,
) {
command.env_clear();
for (key, value) in environment {
if profile.allows_ambient(&key) {
command.env(key, value);
}
}
}
fn apply_configured_profile_from(
command: &mut Command,
profile: SubprocessEnvProfile,
environment: impl IntoIterator<Item = (OsString, OsString)>,
configured: &BTreeMap<String, String>,
) {
apply_profile_from(command, profile, environment);
command.envs(configured);
}
#[cfg(windows)]
fn is_windows_startup_variable(key: &str) -> bool {
["ComSpec", "PATHEXT", "SystemRoot", "WINDIR"]
.iter()
.any(|name| matches_environment_name(key, name))
}
#[cfg(not(windows))]
fn is_windows_startup_variable(_key: &str) -> bool {
false
}