use std::{env, fmt};
use adotenv::load::LoadedEnvironment;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ApplyError {
EmptyKey,
InvalidKey {
key: String,
},
InvalidValue {
key: String,
},
}
impl fmt::Display for ApplyError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyKey => formatter.write_str("environment key is empty"),
Self::InvalidKey { key } => {
write!(formatter, "environment key contains NUL or '=': {key:?}")
}
Self::InvalidValue { key } => {
write!(formatter, "environment value for {key:?} contains NUL")
}
}
}
}
impl std::error::Error for ApplyError {}
pub fn validate(environment: &LoadedEnvironment) -> Result<(), ApplyError> {
for (key, value) in environment.values() {
if key.is_empty() {
return Err(ApplyError::EmptyKey);
}
if key.bytes().any(|byte| byte == 0 || byte == b'=') {
return Err(ApplyError::InvalidKey { key: key.clone() });
}
if value.as_bytes().contains(&0) {
return Err(ApplyError::InvalidValue { key: key.clone() });
}
}
Ok(())
}
pub unsafe fn apply(environment: &LoadedEnvironment) -> Result<(), ApplyError> {
validate(environment)?;
for (key, value) in environment.values() {
unsafe { env::set_var(key, value) };
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{
fs,
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
};
use super::*;
use adotenv::load::load_cascade;
static NEXT: AtomicU64 = AtomicU64::new(0);
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let path = std::env::temp_dir().join(format!(
"adotenv-process-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&path).unwrap();
Self(path)
}
fn path(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn environment(key: &str, value: &str) -> (TempDir, LoadedEnvironment) {
let dir = TempDir::new();
fs::write(dir.path().join(".env"), format!("{key}={value}\n")).unwrap();
let loaded = load_cascade(dir.path(), "").unwrap();
(dir, loaded)
}
#[test]
fn apply_sets_loaded_values_and_can_be_removed() {
let key = format!(
"ADOTENV_PROCESS_{}_{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
);
let (_dir, loaded) = environment(&key, "value");
unsafe { apply(&loaded).unwrap() };
assert_eq!(std::env::var(&key).as_deref(), Ok("value"));
std::env::remove_var(&key);
}
#[test]
fn validation_rejects_nul_values_before_mutation() {
let key = format!(
"ADOTENV_PROCESS_BAD_{}_{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
);
let (_dir, loaded) = environment(&key, "bad\0value");
assert!(matches!(
validate(&loaded),
Err(ApplyError::InvalidValue { .. })
));
assert!(matches!(
unsafe { apply(&loaded) },
Err(ApplyError::InvalidValue { .. })
));
assert!(std::env::var_os(&key).is_none());
}
#[test]
fn error_messages_include_the_key() {
let error = ApplyError::InvalidKey { key: "A=B".into() };
assert!(error.to_string().contains("A=B"));
}
}