use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
use std::ffi::{OsStr, OsString};
use std::hash::{Hash, Hasher};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
use wildmatch::WildMatch;
use crate::CommandError;
use crate::command::contains_nul;
mod model;
use model::{CaseFoldedText, EnvironmentNameIdentity};
pub use model::{
CoreEnvironment, EnvironmentBase, EnvironmentFilterAction, EnvironmentInput,
EnvironmentNameKey, EnvironmentOverride, EnvironmentPattern, EnvironmentSpec,
};
impl PartialEq for EnvironmentPattern {
fn eq(&self, other: &Self) -> bool {
self.canonical == other.canonical
}
}
impl Eq for EnvironmentPattern {}
impl Hash for EnvironmentPattern {
fn hash<H: Hasher>(&self, state: &mut H) {
self.canonical.hash(state);
}
}
impl PartialOrd for EnvironmentPattern {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for EnvironmentPattern {
fn cmp(&self, other: &Self) -> Ordering {
self.canonical.cmp(&other.canonical)
}
}
impl EnvironmentPattern {
pub fn new(pattern: impl Into<String>) -> Result<Self, CommandError> {
let pattern = pattern.into();
if pattern.is_empty() {
return Err(CommandError::EmptyEnvironmentPattern);
}
if contains_nul(OsStr::new(&pattern)) {
return Err(CommandError::EnvironmentPatternContainsNul);
}
if pattern.contains('=') {
return Err(CommandError::EnvironmentPatternContainsEquals);
}
Ok(Self {
canonical: case_folded_text(&pattern),
matcher: WildMatch::new_case_insensitive(&pattern),
original: pattern,
})
}
pub fn as_str(&self) -> &str {
&self.original
}
pub fn matches(&self, name: &str) -> bool {
self.matcher.matches(name)
}
}
impl EnvironmentNameKey {
pub fn new(name: &OsStr) -> Self {
Self(environment_name_identity(name))
}
}
impl CoreEnvironment {
pub fn from_selected<I>(variables: I) -> Result<Self, CommandError>
where
I: IntoIterator<Item = (OsString, OsString)>,
{
Ok(Self {
variables: collect_environment(variables)?,
})
}
pub fn variables(&self) -> &BTreeMap<OsString, OsString> {
&self.variables
}
}
impl EnvironmentInput {
pub fn all<I>(variables: I) -> Result<Self, CommandError>
where
I: IntoIterator<Item = (OsString, OsString)>,
{
Ok(Self {
base: EnvironmentBase::All,
variables: collect_environment(variables)?,
})
}
pub fn core(environment: CoreEnvironment) -> Self {
Self {
base: EnvironmentBase::Core,
variables: environment.variables,
}
}
pub fn empty() -> Self {
Self {
base: EnvironmentBase::None,
variables: BTreeMap::new(),
}
}
pub const fn base(&self) -> EnvironmentBase {
self.base
}
pub fn variables(&self) -> &BTreeMap<OsString, OsString> {
&self.variables
}
pub fn into_variables(self) -> BTreeMap<OsString, OsString> {
self.variables
}
}
impl PartialEq for EnvironmentSpec {
fn eq(&self, other: &Self) -> bool {
self.base == other.base
&& self.filters == other.filters
&& self.override_names.len() == other.override_names.len()
&& self.override_names.iter().all(|(key, name)| {
let Some(other_name) = other.override_names.get(key) else {
return false;
};
self.overrides.get(name) == other.overrides.get(other_name)
})
}
}
impl Eq for EnvironmentSpec {}
impl EnvironmentSpec {
pub fn inherit_all() -> Self {
Self {
base: EnvironmentBase::All,
overrides: BTreeMap::new(),
override_names: HashMap::new(),
filters: BTreeMap::new(),
}
}
pub fn empty() -> Self {
Self {
base: EnvironmentBase::None,
overrides: BTreeMap::new(),
override_names: HashMap::new(),
filters: BTreeMap::new(),
}
}
pub fn inherit_core() -> Self {
Self {
base: EnvironmentBase::Core,
overrides: BTreeMap::new(),
override_names: HashMap::new(),
filters: BTreeMap::new(),
}
}
pub fn base(&self) -> EnvironmentBase {
self.base
}
pub fn overrides(&self) -> &BTreeMap<OsString, EnvironmentOverride> {
&self.overrides
}
pub fn filters(&self) -> &BTreeMap<EnvironmentPattern, EnvironmentFilterAction> {
&self.filters
}
pub fn override_for(&self, name: &OsStr) -> Option<&EnvironmentOverride> {
self.override_names
.get(&EnvironmentNameKey::new(name))
.and_then(|name| self.overrides.get(name))
}
pub fn filter_action_for(&self, name: &str) -> Option<EnvironmentFilterAction> {
let mut include_matches = false;
for (pattern, action) in &self.filters {
if pattern.matches(name) {
match action {
EnvironmentFilterAction::Include => include_matches = true,
EnvironmentFilterAction::Exclude => {
return Some(EnvironmentFilterAction::Exclude);
}
}
}
}
include_matches.then_some(EnvironmentFilterAction::Include)
}
pub fn apply_to(&self, input: EnvironmentInput) -> Result<EnvironmentInput, CommandError> {
if !base_is_at_most(input.base, self.base) {
return Err(CommandError::EnvironmentBaseTooPermissive {
required: self.base,
supplied: input.base,
});
}
let base = input.base;
let variables = input.variables;
let mut environment = BTreeMap::new();
let mut environment_names = HashMap::new();
for (name, value) in variables {
remove_environment_name(&mut environment, &mut environment_names, &name);
environment_names.insert(EnvironmentNameKey::new(&name), name.clone());
environment.insert(name, value);
}
let has_include_filter = self
.filters
.values()
.any(|action| *action == EnvironmentFilterAction::Include);
environment.retain(|name, _| {
self.filters.is_empty()
|| name.to_str().is_some_and(|name| {
!self.filters.iter().any(|(pattern, action)| {
*action == EnvironmentFilterAction::Exclude && pattern.matches(name)
})
})
});
for (name, value) in &self.overrides {
match value {
EnvironmentOverride::Set(value) => {
remove_environment_name(&mut environment, &mut environment_names, name);
environment_names.insert(EnvironmentNameKey::new(name), name.clone());
environment.insert(name.clone(), value.clone());
}
EnvironmentOverride::Remove => {
remove_environment_name(&mut environment, &mut environment_names, name);
}
}
}
if has_include_filter {
environment.retain(|name, _| {
name.to_str().is_some_and(|name| {
self.filters.iter().any(|(pattern, action)| {
*action == EnvironmentFilterAction::Include && pattern.matches(name)
})
})
});
} else if !self.filters.is_empty() {
environment.retain(|name, _| name.to_str().is_some());
}
Ok(EnvironmentInput {
base,
variables: environment,
})
}
pub fn with_var(
mut self,
name: impl Into<OsString>,
value: impl Into<OsString>,
) -> Result<Self, CommandError> {
let name = name.into();
let value = value.into();
validate_name(&name)?;
if contains_nul(&value) {
return Err(CommandError::EnvironmentValueContainsNul);
}
remove_environment_name(&mut self.overrides, &mut self.override_names, &name);
self.override_names
.insert(EnvironmentNameKey::new(&name), name.clone());
self.overrides.insert(name, EnvironmentOverride::Set(value));
Ok(self)
}
pub fn without_var(mut self, name: impl Into<OsString>) -> Result<Self, CommandError> {
let name = name.into();
validate_name(&name)?;
remove_environment_name(&mut self.overrides, &mut self.override_names, &name);
self.override_names
.insert(EnvironmentNameKey::new(&name), name.clone());
self.overrides.insert(name, EnvironmentOverride::Remove);
Ok(self)
}
pub fn with_filter(
mut self,
pattern: impl Into<String>,
action: EnvironmentFilterAction,
) -> Result<Self, CommandError> {
let pattern = EnvironmentPattern::new(pattern)?;
self.filters.remove(&pattern);
self.filters.insert(pattern, action);
Ok(self)
}
pub fn with_include_pattern(self, pattern: impl Into<String>) -> Result<Self, CommandError> {
self.with_filter(pattern, EnvironmentFilterAction::Include)
}
pub fn with_exclude_pattern(self, pattern: impl Into<String>) -> Result<Self, CommandError> {
self.with_filter(pattern, EnvironmentFilterAction::Exclude)
}
}
impl Default for EnvironmentSpec {
fn default() -> Self {
Self::inherit_core()
}
}
fn validate_name(name: &OsStr) -> Result<(), CommandError> {
if name.is_empty() {
return Err(CommandError::EmptyEnvironmentName);
}
if contains_nul(name) {
return Err(CommandError::EnvironmentNameContainsNul);
}
if name.to_string_lossy().contains('=') {
return Err(CommandError::EnvironmentNameContainsEquals);
}
Ok(())
}
fn collect_environment<I>(variables: I) -> Result<BTreeMap<OsString, OsString>, CommandError>
where
I: IntoIterator<Item = (OsString, OsString)>,
{
let mut collected = BTreeMap::new();
let mut names = HashMap::new();
for (name, value) in variables {
validate_name(&name)?;
if contains_nul(&value) {
return Err(CommandError::EnvironmentValueContainsNul);
}
remove_environment_name(&mut collected, &mut names, &name);
names.insert(EnvironmentNameKey::new(&name), name.clone());
collected.insert(name, value);
}
Ok(collected)
}
fn remove_environment_name<V>(
values: &mut BTreeMap<OsString, V>,
names: &mut HashMap<EnvironmentNameKey, OsString>,
name: &OsStr,
) {
if let Some(existing) = names.remove(&EnvironmentNameKey::new(name)) {
values.remove(&existing);
}
}
fn environment_name_identity(name: &OsStr) -> EnvironmentNameIdentity {
if let Some(name) = name.to_str() {
return EnvironmentNameIdentity::Folded(case_folded_text(name));
}
#[cfg(unix)]
{
EnvironmentNameIdentity::NativeBytes(name.as_bytes().to_vec())
}
#[cfg(windows)]
{
EnvironmentNameIdentity::NativeWide(name.encode_wide().collect())
}
}
fn case_folded_text(value: &str) -> CaseFoldedText {
CaseFoldedText(
value
.chars()
.map(|character| character.to_lowercase().collect())
.collect(),
)
}
fn base_is_at_most(supplied: EnvironmentBase, required: EnvironmentBase) -> bool {
match required {
EnvironmentBase::None => supplied == EnvironmentBase::None,
EnvironmentBase::Core => supplied != EnvironmentBase::All,
EnvironmentBase::All => true,
}
}