Skip to main content

geneos_toolkit/
env.rs

1use std::env;
2use std::error::Error;
3use std::fmt;
4
5#[non_exhaustive]
6pub enum EnvError {
7    VarError(env::VarError),
8    IoError(std::io::Error),
9    MissingSecureEnvSupport,
10    #[cfg(feature = "secure-env")]
11    DecryptionFailed(String),
12    #[cfg(feature = "secure-env")]
13    MissingKeyFile,
14    #[cfg(feature = "secure-env")]
15    KeyFileFormatError(String),
16}
17
18impl fmt::Debug for EnvError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            EnvError::VarError(env::VarError::NotUnicode(_)) => {
22                write!(f, "VarError(NotUnicode([REDACTED]))")
23            }
24            EnvError::VarError(e) => f.debug_tuple("VarError").field(e).finish(),
25            EnvError::IoError(e) => f.debug_tuple("IoError").field(e).finish(),
26            EnvError::MissingSecureEnvSupport => write!(f, "MissingSecureEnvSupport"),
27            #[cfg(feature = "secure-env")]
28            EnvError::DecryptionFailed(_) => write!(f, "DecryptionFailed([REDACTED])"),
29            #[cfg(feature = "secure-env")]
30            EnvError::MissingKeyFile => write!(f, "MissingKeyFile"),
31            #[cfg(feature = "secure-env")]
32            EnvError::KeyFileFormatError(_) => write!(f, "KeyFileFormatError([REDACTED])"),
33        }
34    }
35}
36
37impl From<env::VarError> for EnvError {
38    fn from(err: env::VarError) -> Self {
39        EnvError::VarError(err)
40    }
41}
42
43impl From<std::io::Error> for EnvError {
44    fn from(err: std::io::Error) -> Self {
45        EnvError::IoError(err)
46    }
47}
48
49impl fmt::Display for EnvError {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            EnvError::VarError(env::VarError::NotUnicode(_)) => write!(
53                f,
54                "Environment variable error: environment variable value is not valid Unicode"
55            ),
56            EnvError::VarError(e) => write!(f, "Environment variable error: {}", e),
57            EnvError::IoError(e) => write!(f, "IO error: {}", e),
58            EnvError::MissingSecureEnvSupport => {
59                write!(
60                    f,
61                    "Secure environment support is disabled (enable the 'secure-env' feature)"
62                )
63            }
64            #[cfg(feature = "secure-env")]
65            EnvError::DecryptionFailed(_) => write!(f, "decryption failed"),
66            #[cfg(feature = "secure-env")]
67            EnvError::MissingKeyFile => write!(f, "Missing key file for decryption"),
68            #[cfg(feature = "secure-env")]
69            EnvError::KeyFileFormatError(_) => write!(f, "Key file format error"),
70        }
71    }
72}
73
74impl Error for EnvError {
75    fn source(&self) -> Option<&(dyn Error + 'static)> {
76        match self {
77            EnvError::VarError(env::VarError::NotUnicode(_)) => None,
78            EnvError::VarError(e) => Some(e),
79            EnvError::IoError(e) => Some(e),
80            _ => None,
81        }
82    }
83}
84
85/// Retrieves an environment variable's value.
86/// Returns `MissingSecureEnvSupport` if the value is encrypted and `secure-env` is disabled.
87///
88/// # Example (ignored to avoid mutating process env in doctest)
89/// ```ignore
90/// use geneos_toolkit::env::get_var;
91/// std::env::set_var("PLAIN_EXAMPLE", "ok");
92/// assert_eq!(get_var("PLAIN_EXAMPLE").unwrap(), "ok");
93/// ```
94pub fn get_var(name: &str) -> Result<String, EnvError> {
95    let val = env::var(name)?;
96    #[cfg(not(feature = "secure-env"))]
97    if is_encrypted(&val) {
98        return Err(EnvError::MissingSecureEnvSupport);
99    }
100    Ok(val)
101}
102
103/// Retrieves an environment variable's value or returns a default if not set.
104/// Returns an error if the value is encrypted and secure support is disabled.
105///
106/// # Example (ignored to avoid mutating process env in doctest)
107/// ```ignore
108/// use geneos_toolkit::env::get_var_or;
109/// assert_eq!(get_var_or("MISSING", "fallback").unwrap(), "fallback");
110/// ```
111pub fn get_var_or(name: &str, default: &str) -> Result<String, EnvError> {
112    match env::var(name) {
113        Ok(val) => {
114            #[cfg(not(feature = "secure-env"))]
115            if is_encrypted(&val) {
116                return Err(EnvError::MissingSecureEnvSupport);
117            }
118            Ok(val)
119        }
120        Err(env::VarError::NotPresent) => Ok(default.to_string()),
121        Err(e) => Err(EnvError::VarError(e)),
122    }
123}
124
125/// Checks if a string slice is encrypted. Encrypted values start with "+encs+".
126pub fn is_encrypted(value: &str) -> bool {
127    value.starts_with("+encs+")
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use temp_env::with_var;
134
135    #[cfg(unix)]
136    fn invalid_secret_value() -> std::ffi::OsString {
137        use std::os::unix::ffi::OsStringExt;
138
139        std::ffi::OsString::from_vec(b"secret-\xFF-token".to_vec())
140    }
141
142    #[cfg(unix)]
143    fn assert_notunicode_error_redacted(err: &EnvError) {
144        assert!(
145            matches!(err, EnvError::VarError(env::VarError::NotUnicode(_))),
146            "expected NotUnicode EnvError, got {err:?}"
147        );
148        assert_eq!(
149            err.to_string(),
150            "Environment variable error: environment variable value is not valid Unicode"
151        );
152        assert_eq!(format!("{err:?}"), "VarError(NotUnicode([REDACTED]))");
153        assert!(
154            std::error::Error::source(err).is_none(),
155            "NotUnicode source should not expose std::env::VarError"
156        );
157
158        let formatted = format!("{err} {err:?}");
159        assert!(!formatted.contains("secret"));
160        assert!(!formatted.contains("token"));
161    }
162
163    #[test]
164    fn test_get_env() {
165        with_var("TEST_VAR", Some("test_value"), || {
166            assert_eq!(get_var("TEST_VAR").unwrap(), "test_value");
167            assert_eq!(get_var_or("TEST_VAR", "default").unwrap(), "test_value");
168        });
169
170        with_var::<_, &str, _, _>("NON_EXISTENT_VAR", None, || {
171            assert!(get_var("NON_EXISTENT_VAR").is_err());
172            assert_eq!(
173                get_var_or("NON_EXISTENT_VAR", "default").unwrap(),
174                "default"
175            );
176        });
177    }
178
179    #[cfg(unix)]
180    #[test]
181    fn test_notunicode_var_error_redacts_display_debug_and_source() {
182        let err = EnvError::VarError(env::VarError::NotUnicode(invalid_secret_value()));
183        assert_notunicode_error_redacted(&err);
184    }
185
186    #[cfg(unix)]
187    #[test]
188    fn test_get_var_redacts_notunicode_errors() {
189        let name = "GENEOS_TOOLKIT_TEST_INVALID_UNICODE";
190
191        with_var(name, Some(invalid_secret_value()), || {
192            let err = get_var(name).unwrap_err();
193            assert_notunicode_error_redacted(&err);
194
195            let err = get_var_or(name, "default").unwrap_err();
196            assert_notunicode_error_redacted(&err);
197        });
198    }
199
200    #[test]
201    fn test_is_encrypted() {
202        assert!(is_encrypted("+encs+1234567890ABCDEF"));
203        assert!(!is_encrypted("plain_text"));
204        assert!(!is_encrypted(""));
205    }
206
207    #[test]
208    fn test_is_encrypted_edge_cases() {
209        // Bare prefix with no payload is still "encrypted"
210        assert!(is_encrypted("+encs+"));
211
212        // Incomplete prefix variants are not encrypted
213        assert!(!is_encrypted("+encs"));
214        assert!(!is_encrypted("+enc+"));
215        assert!(!is_encrypted("+ENCS+1234"));
216        assert!(!is_encrypted("encs+1234"));
217        assert!(!is_encrypted(" +encs+1234"));
218    }
219}