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
85pub 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
103pub 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
125pub 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 assert!(is_encrypted("+encs+"));
211
212 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}