Skip to main content

adotenv_process/
lib.rs

1//! Explicit process-environment application for loaded `.env` values.
2//!
3//! This crate deliberately lives outside `adotenv`: the core forbids unsafe code
4//! and never mutates the process environment. Callers must use [`apply`]
5//! deliberately, after loading has completed and validation has succeeded.
6
7use std::{env, fmt};
8
9use adotenv::load::LoadedEnvironment;
10
11/// Validation or application failure.
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum ApplyError {
15    /// A key was empty.
16    EmptyKey,
17    /// A key contained a NUL or `=`.
18    InvalidKey {
19        /// Offending key.
20        key: String,
21    },
22    /// A value contained a NUL.
23    InvalidValue {
24        /// Key whose value is invalid.
25        key: String,
26    },
27}
28
29impl fmt::Display for ApplyError {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::EmptyKey => formatter.write_str("environment key is empty"),
33            Self::InvalidKey { key } => {
34                write!(formatter, "environment key contains NUL or '=': {key:?}")
35            }
36            Self::InvalidValue { key } => {
37                write!(formatter, "environment value for {key:?} contains NUL")
38            }
39        }
40    }
41}
42
43impl std::error::Error for ApplyError {}
44
45/// Validate every loaded key and value without mutating the environment.
46///
47/// # Errors
48/// Returns [`ApplyError`] when a key is empty or contains NUL/`=`, or a value
49/// contains NUL.
50pub fn validate(environment: &LoadedEnvironment) -> Result<(), ApplyError> {
51    for (key, value) in environment.values() {
52        if key.is_empty() {
53            return Err(ApplyError::EmptyKey);
54        }
55        if key.bytes().any(|byte| byte == 0 || byte == b'=') {
56            return Err(ApplyError::InvalidKey {
57                key: key.to_string(),
58            });
59        }
60        if value.as_bytes().contains(&0) {
61            return Err(ApplyError::InvalidValue {
62                key: key.to_string(),
63            });
64        }
65    }
66    Ok(())
67}
68
69/// Apply every loaded value to the current process environment.
70///
71/// This sets only the keys present in the loaded environment. It does not clear
72/// or remove any existing process variables. Every key and value is validated
73/// before the first mutation.
74///
75/// # Safety
76/// The process environment is global and not thread-safe on all platforms. The
77/// caller must ensure that no other thread is concurrently reading or writing
78/// the process environment and must accept that process-environment changes
79/// cannot be rolled back. Prefer consuming [`LoadedEnvironment::values`] as a
80/// map unless process-global mutation is actually required.
81///
82/// # Errors
83/// Returns [`ApplyError`] before changing anything when validation fails.
84pub unsafe fn apply(environment: &LoadedEnvironment) -> Result<(), ApplyError> {
85    validate(environment)?;
86    for (key, value) in environment.values() {
87        unsafe { env::set_var(key, value) };
88    }
89    Ok(())
90}
91
92#[cfg(test)]
93mod tests {
94    use std::{
95        fs,
96        path::PathBuf,
97        sync::atomic::{AtomicU64, Ordering},
98    };
99
100    use super::*;
101    use adotenv::load::load_cascade;
102
103    static NEXT: AtomicU64 = AtomicU64::new(0);
104
105    struct TempDir(PathBuf);
106
107    impl TempDir {
108        fn new() -> Self {
109            let path = std::env::temp_dir().join(format!(
110                "adotenv-process-{}-{}",
111                std::process::id(),
112                NEXT.fetch_add(1, Ordering::Relaxed)
113            ));
114            fs::create_dir_all(&path).unwrap();
115            Self(path)
116        }
117        fn path(&self) -> &std::path::Path {
118            &self.0
119        }
120    }
121
122    impl Drop for TempDir {
123        fn drop(&mut self) {
124            let _ = fs::remove_dir_all(&self.0);
125        }
126    }
127
128    fn environment(key: &str, value: &str) -> (TempDir, LoadedEnvironment) {
129        let dir = TempDir::new();
130        fs::write(dir.path().join(".env"), format!("{key}={value}\n")).unwrap();
131        let loaded = load_cascade(dir.path(), "").unwrap();
132        (dir, loaded)
133    }
134
135    #[test]
136    fn apply_sets_loaded_values_and_can_be_removed() {
137        let key = format!(
138            "ADOTENV_PROCESS_{}_{}",
139            std::process::id(),
140            NEXT.fetch_add(1, Ordering::Relaxed)
141        );
142        let (_dir, loaded) = environment(&key, "value");
143        unsafe { apply(&loaded).unwrap() };
144        assert_eq!(std::env::var(&key).as_deref(), Ok("value"));
145        std::env::remove_var(&key);
146    }
147
148    #[test]
149    fn validation_rejects_nul_values_before_mutation() {
150        let key = format!(
151            "ADOTENV_PROCESS_BAD_{}_{}",
152            std::process::id(),
153            NEXT.fetch_add(1, Ordering::Relaxed)
154        );
155        let (_dir, loaded) = environment(&key, "bad\0value");
156        assert!(matches!(
157            validate(&loaded),
158            Err(ApplyError::InvalidValue { .. })
159        ));
160        assert!(matches!(
161            unsafe { apply(&loaded) },
162            Err(ApplyError::InvalidValue { .. })
163        ));
164        assert!(std::env::var_os(&key).is_none());
165    }
166
167    #[test]
168    fn error_messages_include_the_key() {
169        let error = ApplyError::InvalidKey { key: "A=B".into() };
170        assert!(error.to_string().contains("A=B"));
171    }
172}