Skip to main content

running_process_platform_internal/
env.rs

1//! The mechanism for reading declared environment variables (#1101).
2//!
3//! This is *how* a variable is read -- the kinds, the owner, the typed
4//! accessors and the boolean parsers -- with no variable names in it. It lives
5//! here, below `running-process`, so every crate in the workspace can share
6//! one parser instead of growing its own; `running_process::env_vars`
7//! re-exports it and keeps the policy: which variables exist, and what each
8//! means.
9//!
10//! # Why booleans get two accessors rather than one
11//!
12//! "Is this switch on?" has two defensible answers when the value is neither
13//! clearly on nor clearly off, and which one is right depends on who owns the
14//! variable -- not on the call site, which is how a codebase ends up with five
15//! parsers that disagree.
16//!
17//! - [`flag_owned`] is for switches the reader defines. Unknown means **off**.
18//! - [`flag_foreign`] is for values written by someone else. Unknown means
19//!   **on**, so a stray `=0` cannot exempt a process from being reaped.
20//! - [`flag_opt_out`] is for an escape hatch that is on until someone turns it
21//!   off. Unset means **on**.
22//!
23//! All three trim and lowercase before comparing, so `" True "` and `"TRUE"`
24//! agree.
25
26use std::ffi::OsStr;
27
28/// What kind of value a variable carries, and how it is read.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum EnvKind {
31    /// A switch the reader defines. Unknown values are off; see [`flag_owned`].
32    OwnedFlag,
33    /// A switch whose value space belongs to someone else. Unknown values are
34    /// on; see [`flag_foreign`].
35    ForeignFlag,
36    /// An escape hatch that is on unless explicitly turned off. Unset is on;
37    /// see [`flag_opt_out`].
38    OptOutFlag,
39    /// A switch that is on for exactly one spelling and off for every other,
40    /// including plausible ones. Reserved for guards where honouring a
41    /// misspelling would be the dangerous direction.
42    ExactValue(&'static str),
43    /// A filesystem path.
44    Path,
45    /// Free text -- a name, scope, endpoint, or token.
46    Text,
47    /// A number: a count, a timeout in milliseconds, a port, a descriptor.
48    ///
49    /// `zero_selects_default` records what `0` means for *this* variable,
50    /// because it is not the same answer everywhere. A connect timeout of zero
51    /// makes every connection fail instantly and is never what anyone wants,
52    /// so zero falls back to the default. A drain timeout of zero means "do
53    /// not wait", which is a perfectly reasonable thing to ask for, so zero is
54    /// honoured. Leaving this unstated is how the two ended up parsed
55    /// differently by accident.
56    Number { zero_selects_default: bool },
57}
58
59/// Who decides what values a variable may take.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Owner {
62    /// Defined by the reader; the value space is ours.
63    Crate,
64    /// Set by a supervising process or a test harness; we only read it.
65    Foreign,
66}
67
68/// One environment variable the reader reads.
69#[derive(Debug, Clone, Copy)]
70pub struct EnvVar {
71    /// The variable name as it appears in the environment.
72    pub name: &'static str,
73    /// What the value means and how it is parsed.
74    pub kind: EnvKind,
75    /// Who owns the value space.
76    pub owner: Owner,
77    /// What happens when the variable is unset.
78    pub default: &'static str,
79    /// One line an embedder can read to know whether they care.
80    pub summary: &'static str,
81}
82
83impl EnvVar {
84    /// Read this variable as a boolean, using the semantics it declares.
85    ///
86    /// # Panics
87    /// If the variable is not declared as a flag. That is a programming error
88    /// in the reader, caught by `an_unset_flag_matches_its_declared_default`,
89    /// not something a value in the environment can cause.
90    pub fn is_set(&self) -> bool {
91        match self.kind {
92            EnvKind::OwnedFlag => flag_owned(self.name),
93            EnvKind::ForeignFlag => flag_foreign(self.name),
94            EnvKind::OptOutFlag => flag_opt_out(self.name),
95            EnvKind::ExactValue(expected) => {
96                std::env::var_os(self.name).is_some_and(|value| value == OsStr::new(expected))
97            }
98            other => panic!("{} is declared as {other:?}, not a flag", self.name),
99        }
100    }
101}
102
103impl EnvVar {
104    /// Read this variable as a count, falling back to `default`.
105    ///
106    /// A value that is not a number is not a smaller number: it is a mistake,
107    /// and the default is a better answer than a silently-wrong one.
108    pub fn count_or(&self, default: usize) -> usize {
109        self.parsed::<usize>().unwrap_or(default)
110    }
111
112    /// Read this variable as a millisecond duration, falling back to `default`.
113    pub fn millis_or(&self, default: std::time::Duration) -> std::time::Duration {
114        self.parsed::<u64>()
115            .map(std::time::Duration::from_millis)
116            .unwrap_or(default)
117    }
118
119    /// Read this variable as a port number, if it names one.
120    pub fn port(&self) -> Option<u16> {
121        self.parsed::<u16>()
122    }
123
124    /// Whether the variable is present at all, whatever its value -- including
125    /// empty or `0`.
126    ///
127    /// Several older switches are read this way, and narrowing them to
128    /// recognised spellings would turn `=0` from "on" into "off" for anyone who
129    /// already relies on it. It is a method rather than an [`EnvKind`] variant
130    /// because `EnvKind` is public and exhaustive: adding a variant would break
131    /// a downstream `match` in a 4.x release.
132    pub fn is_present(&self) -> bool {
133        std::env::var_os(self.name).is_some()
134    }
135
136    /// Read this variable as text, if it is set to anything.
137    pub fn text(&self) -> Option<String> {
138        std::env::var(self.name)
139            .ok()
140            .filter(|value| !value.is_empty())
141    }
142
143    /// Read this variable exactly as the host wrote it.
144    ///
145    /// Unlike [`EnvVar::path`] an empty value is still `Some`: some readers
146    /// have always treated `VAR=` as set, and this keeps them doing so.
147    pub fn os(&self) -> Option<std::ffi::OsString> {
148        std::env::var_os(self.name)
149    }
150
151    /// Read this variable as Unicode exactly as written, empty included.
152    ///
153    /// `None` when unset *or* not valid Unicode, matching `std::env::var`.
154    pub fn string(&self) -> Option<String> {
155        std::env::var(self.name).ok()
156    }
157
158    /// Read this variable as a path, if it is set to anything.
159    ///
160    /// Takes the value as the host wrote it: a path that is not valid Unicode
161    /// is still a path, and lossily repairing it would point somewhere else.
162    pub fn path(&self) -> Option<std::path::PathBuf> {
163        std::env::var_os(self.name)
164            .filter(|value| !value.is_empty())
165            .map(std::path::PathBuf::from)
166    }
167
168    /// Parse the value, applying this variable's declared rule for zero.
169    ///
170    /// # Panics
171    /// If the variable is not declared as a number. A programming error in
172    /// the reader, not something the environment can cause.
173    fn parsed<T>(&self) -> Option<T>
174    where
175        T: std::str::FromStr + Default + PartialEq,
176    {
177        let EnvKind::Number {
178            zero_selects_default,
179        } = self.kind
180        else {
181            panic!("{} is declared as {:?}, not a number", self.name, self.kind);
182        };
183        let parsed: T = std::env::var(self.name)
184            .ok()?
185            .trim()
186            .parse()
187            .ok()
188            .filter(|value: &T| !(zero_selects_default && *value == T::default()))?;
189        Some(parsed)
190    }
191}
192
193/// Spellings that turn an owned switch on. Anything else, including an
194/// unrecognised value, leaves it off.
195const AFFIRMATIVE: &[&str] = &["1", "true", "yes", "on"];
196
197/// Spellings that turn a foreign switch off. Anything else, including an
198/// unrecognised value, leaves it on.
199const NEGATIVE: &[&str] = &["", "0", "false", "no", "off"];
200
201/// Read a switch the reader owns: on only for a recognised affirmative.
202pub fn flag_owned(name: &str) -> bool {
203    match std::env::var_os(name) {
204        Some(value) => AFFIRMATIVE.contains(&normalize(&value).as_str()),
205        None => false,
206    }
207}
208
209/// Read a switch someone else writes: off only for a recognised negative.
210///
211/// Unset is still off -- absence is not a value, and reading it as "on" would
212/// make every process claim every marker.
213pub fn flag_foreign(name: &str) -> bool {
214    match std::env::var_os(name) {
215        Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
216        None => false,
217    }
218}
219
220/// Read an escape hatch that is on unless turned off.
221///
222/// Unset is *on*, which is what separates this from [`flag_foreign`]: the
223/// caller is asking whether the default behaviour still applies, and it does
224/// until someone says otherwise. Every recognised falsy spelling opens the
225/// hatch, so a user who reaches for `=false` or `=off` gets the fallback they
226/// were plainly asking for rather than silently keeping the default.
227pub fn flag_opt_out(name: &str) -> bool {
228    match std::env::var_os(name) {
229        Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
230        None => true,
231    }
232}
233
234/// Read a variable whose *name* is caller-supplied data, exactly as the host
235/// wrote it.
236///
237/// For the few readers that are handed a name rather than choosing one -- a
238/// descriptor-passing key agreed with a parent, an embedder's disclosure
239/// allowlist. There is nothing to declare for those: the variable belongs to
240/// whoever supplied the name. Every other read goes through a declared
241/// [`EnvVar`]; the `running_process_env_direct` Dylint lint rejects a string
242/// literal passed here, so this cannot become a way around declaring one.
243pub fn os_named(name: &str) -> Option<std::ffi::OsString> {
244    std::env::var_os(name)
245}
246
247/// [`os_named`] as Unicode: `None` when unset *or* not valid Unicode,
248/// matching `std::env::var`.
249pub fn string_named(name: &str) -> Option<String> {
250    std::env::var(name).ok()
251}
252
253/// Whether a *value already in hand* reads as a foreign switch being on.
254///
255/// Callers that scan another process's environment block have the value
256/// without being able to read it from their own environment.
257pub fn value_is_affirmative_foreign(value: &str) -> bool {
258    !NEGATIVE.contains(&value.trim().to_ascii_lowercase().as_str())
259}
260
261/// Declare environment variables as `EnvVar` constants plus a table of all of
262/// them, so a crate's inventory is one list rather than scattered literals.
263///
264/// ```ignore
265/// declare_env_vars! {
266///     /// The table's doc.
267///     pub const TABLE;
268///     HOME => "HOME", EnvKind::Path, Owner::Foreign, "unset", "Home dir.";
269/// }
270/// ```
271#[macro_export]
272macro_rules! declare_env_vars {
273    (
274        $(#[$table_meta:meta])*
275        pub const $table:ident;
276        $($ident:ident => $name:literal, $kind:expr, $owner:expr, $default:literal, $summary:literal;)*
277    ) => {
278        $(
279            #[doc = $summary]
280            ///
281            #[doc = concat!("Environment variable `", $name, "`. Unset: ", $default, ".")]
282            pub const $ident: $crate::env::EnvVar = $crate::env::EnvVar {
283                name: $name,
284                kind: $kind,
285                owner: $owner,
286                default: $default,
287                summary: $summary,
288            };
289        )*
290
291        $(#[$table_meta])*
292        pub const $table: &[$crate::env::EnvVar] = &[$($ident),*];
293    };
294}
295
296fn normalize(value: &OsStr) -> String {
297    value.to_string_lossy().trim().to_ascii_lowercase()
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    /// These tests mutate the real process environment, so two of them setting
305    /// the same variable at once would read each other's value.
306    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
307
308    fn with_var<T>(name: &str, value: Option<&str>, body: impl FnOnce() -> T) -> T {
309        let _guard = ENV_LOCK
310            .lock()
311            .unwrap_or_else(std::sync::PoisonError::into_inner);
312        let previous = std::env::var_os(name);
313        match value {
314            Some(value) => std::env::set_var(name, value),
315            None => std::env::remove_var(name),
316        }
317        let outcome = body();
318        match previous {
319            Some(previous) => std::env::set_var(name, previous),
320            None => std::env::remove_var(name),
321        }
322        outcome
323    }
324
325    const PROBE: &str = "PLATFORM_INTERNAL_ENV_MECHANISM_PROBE";
326
327    fn var(kind: EnvKind) -> EnvVar {
328        EnvVar {
329            name: PROBE,
330            kind,
331            owner: Owner::Crate,
332            default: "unset",
333            summary: "test probe",
334        }
335    }
336
337    #[test]
338    fn the_three_flag_readers_disagree_only_on_unrecognised_values_and_unset() {
339        for (value, owned, foreign, opt_out) in [
340            (Some("1"), true, true, true),
341            (Some(" TRUE "), true, true, true),
342            (Some("0"), false, false, false),
343            (Some("off"), false, false, false),
344            (Some("maybe"), false, true, true),
345            (None, false, false, true),
346        ] {
347            assert_eq!(
348                with_var(PROBE, value, || flag_owned(PROBE)),
349                owned,
350                "{value:?}"
351            );
352            assert_eq!(
353                with_var(PROBE, value, || flag_foreign(PROBE)),
354                foreign,
355                "{value:?}"
356            );
357            assert_eq!(
358                with_var(PROBE, value, || flag_opt_out(PROBE)),
359                opt_out,
360                "{value:?}"
361            );
362        }
363    }
364
365    #[test]
366    fn a_declared_flag_reads_through_its_own_semantics() {
367        assert!(with_var(PROBE, Some("yes"), || var(EnvKind::OwnedFlag).is_set()));
368        assert!(!with_var(PROBE, None, || var(EnvKind::OwnedFlag).is_set()));
369        assert!(with_var(PROBE, None, || var(EnvKind::OptOutFlag).is_set()));
370        assert!(with_var(PROBE, Some("exactly"), || {
371            var(EnvKind::ExactValue("exactly")).is_set()
372        }));
373        assert!(!with_var(PROBE, Some("Exactly"), || {
374            var(EnvKind::ExactValue("exactly")).is_set()
375        }));
376    }
377
378    #[test]
379    fn numbers_honour_the_declared_meaning_of_zero_and_reject_garbage() {
380        let selects = var(EnvKind::Number {
381            zero_selects_default: true,
382        });
383        let honours = var(EnvKind::Number {
384            zero_selects_default: false,
385        });
386        assert_eq!(with_var(PROBE, Some("0"), || selects.count_or(9)), 9);
387        assert_eq!(with_var(PROBE, Some("0"), || honours.count_or(9)), 0);
388        assert_eq!(with_var(PROBE, Some(" 42 "), || honours.count_or(9)), 42);
389        assert_eq!(with_var(PROBE, Some("many"), || honours.count_or(9)), 9);
390        assert_eq!(with_var(PROBE, Some("8080"), || honours.port()), Some(8080));
391        assert_eq!(
392            with_var(PROBE, Some("250"), || {
393                honours.millis_or(std::time::Duration::from_secs(1))
394            }),
395            std::time::Duration::from_millis(250)
396        );
397    }
398
399    #[test]
400    fn text_and_path_treat_empty_as_absent_and_keep_the_value_verbatim() {
401        let text = var(EnvKind::Text);
402        assert_eq!(with_var(PROBE, Some(""), || text.text()), None);
403        assert_eq!(
404            with_var(PROBE, Some("a b"), || text.text()),
405            Some("a b".into())
406        );
407        assert_eq!(with_var(PROBE, Some(""), || text.path()), None);
408        assert_eq!(
409            with_var(PROBE, Some("/x/y"), || text.path()),
410            Some(std::path::PathBuf::from("/x/y"))
411        );
412    }
413
414    #[test]
415    fn os_and_string_keep_an_empty_value_as_set() {
416        let text = var(EnvKind::Text);
417        assert_eq!(
418            with_var(PROBE, Some(""), || text.os()),
419            Some(std::ffi::OsString::new())
420        );
421        assert_eq!(
422            with_var(PROBE, Some(""), || text.string()),
423            Some(String::new())
424        );
425        assert_eq!(with_var(PROBE, None, || text.os()), None);
426        assert_eq!(with_var(PROBE, None, || text.string()), None);
427    }
428
429    #[test]
430    fn a_presence_switch_is_on_for_any_value_even_empty_or_zero() {
431        let presence = var(EnvKind::Text);
432        for value in ["", "0", "off", "1"] {
433            assert!(
434                with_var(PROBE, Some(value), || presence.is_present()),
435                "{value:?}"
436            );
437        }
438        assert!(!with_var(PROBE, None, || presence.is_present()));
439    }
440
441    #[test]
442    #[should_panic(expected = "not a flag")]
443    fn reading_a_non_flag_as_a_flag_is_a_programming_error() {
444        with_var(PROBE, Some("1"), || var(EnvKind::Text).is_set());
445    }
446
447    #[test]
448    fn a_value_already_in_hand_reads_like_the_foreign_flag() {
449        for value in ["1", "true", "maybe"] {
450            assert!(value_is_affirmative_foreign(value), "{value}");
451        }
452        for value in ["", "0", " OFF "] {
453            assert!(!value_is_affirmative_foreign(value), "{value}");
454        }
455    }
456}