use std::collections::BTreeMap;
use std::path::Path;
use crate::error::HarnessError;
const PATH_VARIABLE: &str = "PATH";
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EnvironmentError {
#[error(
"an environment pass-through entry has an empty variable name; every entry names one \
variable to carry into the harness child"
)]
EmptyName,
#[error(
"the environment pass-through entry `{entry}` contains `=`; entries are variable NAMES \
only — the value is taken from the launching context, so a KEY=VALUE pair here is a \
mistake to correct rather than reinterpret"
)]
NameContainsEquals {
entry: String,
},
#[error(
"cannot launch `{program}`: it is named without a path, so it is looked up on \
${variable} — and ${variable} is not in the harness `env_pass` declaration, which \
carries {declared}. Add \"{variable}\" to `env_pass`, or give `command` an absolute path."
)]
MissingForExec {
variable: &'static str,
program: String,
declared: String,
},
}
impl From<EnvironmentError> for HarnessError {
fn from(error: EnvironmentError) -> Self {
Self::configuration(error.to_string())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnvironmentDeclaration {
names: Vec<String>,
}
impl EnvironmentDeclaration {
pub fn new(
names: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, EnvironmentError> {
let names = names.into_iter().map(Into::into).collect::<Vec<_>>();
for name in &names {
if name.trim().is_empty() {
return Err(EnvironmentError::EmptyName);
}
if name.contains('=') {
return Err(EnvironmentError::NameContainsEquals {
entry: name.clone(),
});
}
}
Ok(Self { names })
}
#[must_use]
pub fn empty() -> Self {
Self { names: Vec::new() }
}
#[must_use]
pub fn names(&self) -> &[String] {
&self.names
}
#[must_use]
pub fn resolve(&self, context: &BTreeMap<String, String>) -> ChildEnvironment {
let mut pairs = Vec::new();
let mut absent = Vec::new();
let mut seen = Vec::new();
for name in &self.names {
if seen.iter().any(|already| already == name) {
continue;
}
seen.push(name.clone());
match context.get(name) {
Some(value) => pairs.push((name.clone(), value.clone())),
None => absent.push(name.clone()),
}
}
ChildEnvironment {
declared: self.names.clone(),
pairs,
absent,
}
}
#[must_use]
pub fn resolve_from_process(&self) -> ChildEnvironment {
self.resolve(&process_environment())
}
}
#[must_use]
pub fn process_environment() -> BTreeMap<String, String> {
std::env::vars_os()
.filter_map(|(name, value)| Some((name.into_string().ok()?, value.into_string().ok()?)))
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChildEnvironment {
declared: Vec<String>,
pairs: Vec<(String, String)>,
absent: Vec<String>,
}
impl ChildEnvironment {
#[must_use]
pub fn pairs(&self) -> &[(String, String)] {
&self.pairs
}
#[must_use]
pub fn declared(&self) -> &[String] {
&self.declared
}
#[must_use]
pub fn absent(&self) -> &[String] {
&self.absent
}
#[must_use]
pub fn carries(&self, name: &str) -> bool {
self.pairs.iter().any(|(key, _)| key == name)
}
pub fn require_for_program(&self, program: &Path) -> Result<(), EnvironmentError> {
if program.components().count() > 1 || program.is_absolute() {
return Ok(());
}
if self.carries(PATH_VARIABLE) {
return Ok(());
}
Err(EnvironmentError::MissingForExec {
variable: PATH_VARIABLE,
program: program.display().to_string(),
declared: self.rendered_declaration(),
})
}
fn rendered_declaration(&self) -> String {
if self.declared.is_empty() {
return "no variables".to_owned();
}
self.declared.join(", ")
}
}
#[cfg(test)]
mod tests;