Skip to main content

geneos_toolkit/
env.rs

1use std::env;
2use std::error::Error;
3use std::fmt;
4
5pub enum EnvError {
6    VarError(env::VarError),
7    IoError(std::io::Error),
8    MissingSecureEnvSupport,
9    #[cfg(feature = "secure-env")]
10    DecryptionFailed(String),
11    #[cfg(feature = "secure-env")]
12    MissingKeyFile,
13    #[cfg(feature = "secure-env")]
14    KeyFileFormatError(String),
15}
16
17impl fmt::Debug for EnvError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            EnvError::VarError(e) => f.debug_tuple("VarError").field(e).finish(),
21            EnvError::IoError(e) => f.debug_tuple("IoError").field(e).finish(),
22            EnvError::MissingSecureEnvSupport => write!(f, "MissingSecureEnvSupport"),
23            #[cfg(feature = "secure-env")]
24            EnvError::DecryptionFailed(_) => write!(f, "DecryptionFailed([REDACTED])"),
25            #[cfg(feature = "secure-env")]
26            EnvError::MissingKeyFile => write!(f, "MissingKeyFile"),
27            #[cfg(feature = "secure-env")]
28            EnvError::KeyFileFormatError(_) => write!(f, "KeyFileFormatError([REDACTED])"),
29        }
30    }
31}
32
33impl From<env::VarError> for EnvError {
34    fn from(err: env::VarError) -> Self {
35        EnvError::VarError(err)
36    }
37}
38
39impl From<std::io::Error> for EnvError {
40    fn from(err: std::io::Error) -> Self {
41        EnvError::IoError(err)
42    }
43}
44
45impl fmt::Display for EnvError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            EnvError::VarError(e) => write!(f, "Environment variable error: {}", e),
49            EnvError::IoError(e) => write!(f, "IO error: {}", e),
50            EnvError::MissingSecureEnvSupport => {
51                write!(
52                    f,
53                    "Secure environment support is disabled (enable the 'secure-env' feature)"
54                )
55            }
56            #[cfg(feature = "secure-env")]
57            EnvError::DecryptionFailed(_) => write!(f, "decryption failed"),
58            #[cfg(feature = "secure-env")]
59            EnvError::MissingKeyFile => write!(f, "Missing key file for decryption"),
60            #[cfg(feature = "secure-env")]
61            EnvError::KeyFileFormatError(msg) => write!(f, "Key file format error: {}", msg),
62        }
63    }
64}
65
66impl Error for EnvError {
67    fn source(&self) -> Option<&(dyn Error + 'static)> {
68        match self {
69            EnvError::VarError(e) => Some(e),
70            EnvError::IoError(e) => Some(e),
71            _ => None,
72        }
73    }
74}
75
76/// Retrieves an environment variable's value.
77/// Returns `MissingSecureEnvSupport` if the value is encrypted and `secure-env` is disabled.
78///
79/// # Example (ignored to avoid mutating process env in doctest)
80/// ```ignore
81/// use geneos_toolkit::env::get_var;
82/// std::env::set_var("PLAIN_EXAMPLE", "ok");
83/// assert_eq!(get_var("PLAIN_EXAMPLE").unwrap(), "ok");
84/// ```
85pub fn get_var(name: &str) -> Result<String, EnvError> {
86    let val = env::var(name)?;
87    #[cfg(not(feature = "secure-env"))]
88    if is_encrypted(&val) {
89        return Err(EnvError::MissingSecureEnvSupport);
90    }
91    Ok(val)
92}
93
94/// Retrieves an environment variable's value or returns a default if not set.
95/// Returns an error if the value is encrypted and secure support is disabled.
96///
97/// # Example (ignored to avoid mutating process env in doctest)
98/// ```ignore
99/// use geneos_toolkit::env::get_var_or;
100/// assert_eq!(get_var_or("MISSING", "fallback").unwrap(), "fallback");
101/// ```
102pub fn get_var_or(name: &str, default: &str) -> Result<String, EnvError> {
103    match env::var(name) {
104        Ok(val) => {
105            #[cfg(not(feature = "secure-env"))]
106            if is_encrypted(&val) {
107                return Err(EnvError::MissingSecureEnvSupport);
108            }
109            Ok(val)
110        }
111        Err(env::VarError::NotPresent) => Ok(default.to_string()),
112        Err(e) => Err(EnvError::VarError(e)),
113    }
114}
115
116/// Checks if a string slice is encrypted. Encrypted values start with "+encs+".
117pub fn is_encrypted(value: &str) -> bool {
118    value.starts_with("+encs+")
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use temp_env::with_var;
125
126    #[test]
127    fn test_get_env() {
128        with_var("TEST_VAR", Some("test_value"), || {
129            assert_eq!(get_var("TEST_VAR").unwrap(), "test_value");
130            assert_eq!(get_var_or("TEST_VAR", "default").unwrap(), "test_value");
131        });
132
133        with_var::<_, &str, _, _>("NON_EXISTENT_VAR", None, || {
134            assert!(get_var("NON_EXISTENT_VAR").is_err());
135            assert_eq!(
136                get_var_or("NON_EXISTENT_VAR", "default").unwrap(),
137                "default"
138            );
139        });
140    }
141
142    #[test]
143    fn test_is_encrypted() {
144        assert!(is_encrypted("+encs+1234567890ABCDEF"));
145        assert!(!is_encrypted("plain_text"));
146        assert!(!is_encrypted(""));
147    }
148
149    #[test]
150    fn test_is_encrypted_edge_cases() {
151        // Bare prefix with no payload is still "encrypted"
152        assert!(is_encrypted("+encs+"));
153
154        // Incomplete prefix variants are not encrypted
155        assert!(!is_encrypted("+encs"));
156        assert!(!is_encrypted("+enc+"));
157        assert!(!is_encrypted("+ENCS+1234"));
158        assert!(!is_encrypted("encs+1234"));
159        assert!(!is_encrypted(" +encs+1234"));
160    }
161}