Skip to main content

khive_db/
env.rs

1//! Small environment readers for parsed defaults and explicit boolean flags.
2//!
3//! These helpers do not emit configuration audit events or apply caller-specific
4//! range constraints. Presence-only flags and non-Unicode-sensitive policies
5//! need their existing readers.
6
7use std::str::FromStr;
8use std::sync::OnceLock;
9
10/// Read once and parse the exact Unicode value, without trimming or caching.
11///
12/// Missing, non-Unicode and unparseable values use `default`. Values accepted
13/// by `T::from_str`, including non-finite floats, need any caller-specific checks.
14pub fn env_parse_or<T: FromStr>(name: &str, default: T) -> T {
15    std::env::var(name)
16        .ok()
17        .and_then(|value| value.parse().ok())
18        .unwrap_or(default)
19}
20
21/// Read a trimmed, ASCII-case-insensitive explicit boolean flag.
22///
23/// `1`, `true`, `yes` and `on` enable; `0`, `false`, `no` and `off` disable.
24/// Missing, non-Unicode and unrecognized values use `default`.
25pub fn env_flag(name: &str, default: bool) -> bool {
26    std::env::var(name)
27        .ok()
28        .map(|value| match value.trim().to_ascii_lowercase().as_str() {
29            "1" | "true" | "yes" | "on" => true,
30            "0" | "false" | "no" | "off" => false,
31            _ => default,
32        })
33        .unwrap_or(default)
34}
35
36/// Resolve an explicit flag once using the caller's process-lifetime cell.
37///
38/// Dedicate each cell to one setting. The first initialization fixes its value;
39/// later changes to the environment, name or default do not replace that value.
40/// Callers that require a configuration audit event must retain that policy.
41pub fn cached_env_flag(name: &str, default: bool, cache: &'static OnceLock<bool>) -> bool {
42    *cache.get_or_init(|| env_flag(name, default))
43}
44
45#[cfg(test)]
46mod tests {
47    use super::{cached_env_flag, env_flag, env_parse_or};
48    use crate::test_process::run_in_child;
49    use std::sync::OnceLock;
50
51    const KEY: &str = "KHIVE_DB_ENV_HELPERS_TEST_VALUE";
52
53    #[test]
54    fn parsed_defaults_preserve_exact_input_and_read_each_time() {
55        if run_in_child(|command| {
56            command.env_remove(KEY);
57        }) {
58            return;
59        }
60        assert_eq!(env_parse_or(KEY, 17_u64), 17);
61        crate::test_process::set_var(KEY, "42");
62        assert_eq!(env_parse_or(KEY, 17_u64), 42);
63        crate::test_process::set_var(KEY, "-7");
64        assert_eq!(env_parse_or(KEY, 17_i64), -7);
65        assert_eq!(env_parse_or(KEY, 17_u64), 17);
66        for value in ["", " 42 ", "invalid", "18446744073709551616"] {
67            crate::test_process::set_var(KEY, value);
68            assert_eq!(env_parse_or(KEY, 17_u64), 17, "{value:?}");
69        }
70        crate::test_process::set_var(KEY, "256");
71        assert_eq!(env_parse_or(KEY, 9_u8), 9);
72        crate::test_process::set_var(KEY, "NaN");
73        assert!(env_parse_or(KEY, 1.0_f64).is_nan());
74        crate::test_process::set_var(KEY, "41");
75        assert_eq!(env_parse_or(KEY, 17_u64), 41);
76    }
77
78    #[test]
79    fn flags_keep_explicit_false_distinct_from_fallback() {
80        if run_in_child(|command| {
81            command.env_remove(KEY);
82        }) {
83            return;
84        }
85        assert!(!env_flag(KEY, false));
86        assert!(env_flag(KEY, true));
87        for value in ["1", "true", "yes", "on", " TrUe ", "\tYES\n"] {
88            crate::test_process::set_var(KEY, value);
89            assert!(env_flag(KEY, false), "{value:?}");
90        }
91        for value in ["0", "false", "no", "off", " FaLsE ", "\tOFF\n"] {
92            crate::test_process::set_var(KEY, value);
93            assert!(!env_flag(KEY, true), "{value:?}");
94        }
95        for value in ["", " ", "enabled", "2"] {
96            crate::test_process::set_var(KEY, value);
97            assert!(!env_flag(KEY, false), "{value:?}");
98            assert!(env_flag(KEY, true), "{value:?}");
99        }
100    }
101
102    #[test]
103    fn cached_flags_keep_the_first_value_and_separate_cells() {
104        static FIRST: OnceLock<bool> = OnceLock::new();
105        static SECOND: OnceLock<bool> = OnceLock::new();
106        if run_in_child(|command| {
107            command.env_remove(KEY);
108        }) {
109            return;
110        }
111        assert!(cached_env_flag(KEY, true, &FIRST));
112        crate::test_process::set_var(KEY, "false");
113        assert!(!env_flag(KEY, true));
114        assert!(cached_env_flag(KEY, false, &FIRST));
115        assert!(!cached_env_flag(KEY, true, &SECOND));
116        crate::test_process::set_var(KEY, "true");
117        assert!(env_flag(KEY, false));
118        assert!(!cached_env_flag(KEY, false, &SECOND));
119        crate::test_process::remove_var(KEY);
120        assert!(!cached_env_flag(KEY, true, &SECOND));
121    }
122
123    #[cfg(unix)]
124    #[test]
125    fn non_unicode_values_use_the_supplied_defaults() {
126        use std::ffi::OsString;
127        use std::os::unix::ffi::OsStringExt;
128        static CACHE: OnceLock<bool> = OnceLock::new();
129        if run_in_child(|command| {
130            command.env_remove(KEY);
131        }) {
132            return;
133        }
134        crate::test_process::set_var(KEY, OsString::from_vec(vec![0xff]));
135        assert_eq!(env_parse_or(KEY, 23_u32), 23);
136        assert!(!env_flag(KEY, false));
137        assert!(env_flag(KEY, true));
138        assert!(cached_env_flag(KEY, true, &CACHE));
139    }
140}