1use std::io::ErrorKind;
22
23const DEFAULT_SESSION_TOKEN_FILE: &str = "/run/ati/session_token";
24
25pub(crate) fn default_token_file(env_name: &str) -> String {
35 if env_name == "ATI_SESSION_TOKEN" {
36 return DEFAULT_SESSION_TOKEN_FILE.to_string();
37 }
38 let trimmed = env_name
39 .strip_suffix("_SESSION_TOKEN")
40 .or_else(|| env_name.strip_suffix("_session_token"))
41 .unwrap_or(env_name);
42 format!("/run/ati/{}", trimmed.to_lowercase())
43}
44
45pub fn resolve_token(env_name: &str) -> Result<Option<String>, String> {
58 if let Ok(raw) = std::env::var(env_name) {
59 let trimmed = raw.trim();
60 if !trimmed.is_empty() {
61 return Ok(Some(trimmed.to_string()));
62 }
63 }
64
65 let file_env = format!("{env_name}_FILE");
66 let path = std::env::var(&file_env)
67 .ok()
68 .map(|s| s.trim().to_string())
69 .filter(|s| !s.is_empty())
70 .unwrap_or_else(|| default_token_file(env_name));
71
72 match std::fs::read_to_string(&path) {
73 Ok(contents) => {
74 let trimmed = contents.trim();
75 if trimmed.is_empty() {
76 Ok(None)
77 } else {
78 Ok(Some(trimmed.to_string()))
79 }
80 }
81 Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
82 Err(e) => Err(format!("Cannot read {path}: {e}")),
83 }
84}
85
86pub fn resolve_session_token() -> Result<Option<String>, String> {
90 resolve_token("ATI_SESSION_TOKEN")
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use std::sync::Mutex;
97
98 static ENV_LOCK: Mutex<()> = Mutex::new(());
101
102 struct EnvGuard {
103 keys: Vec<&'static str>,
104 prev: Vec<(String, Option<String>)>,
105 }
106
107 impl EnvGuard {
108 fn set(pairs: &[(&'static str, Option<&str>)]) -> Self {
109 let mut prev = Vec::new();
110 let mut keys = Vec::new();
111 for (k, v) in pairs {
112 prev.push(((*k).to_string(), std::env::var(k).ok()));
113 keys.push(*k);
114 match v {
115 Some(val) => std::env::set_var(k, val),
116 None => std::env::remove_var(k),
117 }
118 }
119 Self { keys, prev }
120 }
121 }
122
123 impl Drop for EnvGuard {
124 fn drop(&mut self) {
125 for (k, v) in &self.prev {
126 match v {
127 Some(val) => std::env::set_var(k, val),
128 None => std::env::remove_var(k),
129 }
130 }
131 let _ = &self.keys;
133 }
134 }
135
136 #[test]
137 fn env_var_wins_over_file() {
138 let _g = ENV_LOCK.lock().unwrap();
139 let dir = tempfile::tempdir().unwrap();
140 let path = dir.path().join("tok");
141 std::fs::write(&path, "from-file").unwrap();
142 let _e = EnvGuard::set(&[
143 ("ATI_SESSION_TOKEN", Some("from-env")),
144 ("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
145 ]);
146 assert_eq!(
147 resolve_session_token().unwrap(),
148 Some("from-env".to_string())
149 );
150 }
151
152 #[test]
153 fn empty_env_falls_through_to_file_and_rereads() {
154 let _g = ENV_LOCK.lock().unwrap();
155 let dir = tempfile::tempdir().unwrap();
156 let path = dir.path().join("tok");
157 std::fs::write(&path, "tok-v1").unwrap();
158 let _e = EnvGuard::set(&[
159 ("ATI_SESSION_TOKEN", Some("")),
160 ("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
161 ]);
162 assert_eq!(resolve_session_token().unwrap(), Some("tok-v1".to_string()));
163
164 std::fs::write(&path, "tok-v2").unwrap();
166 assert_eq!(resolve_session_token().unwrap(), Some("tok-v2".to_string()));
167 }
168
169 #[test]
170 fn trims_whitespace_in_file_contents() {
171 let _g = ENV_LOCK.lock().unwrap();
172 let dir = tempfile::tempdir().unwrap();
173 let path = dir.path().join("tok");
174 std::fs::write(&path, " hello-tok\n\n").unwrap();
175 let _e = EnvGuard::set(&[
176 ("ATI_SESSION_TOKEN", None),
177 ("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
178 ]);
179 assert_eq!(
180 resolve_session_token().unwrap(),
181 Some("hello-tok".to_string())
182 );
183 }
184
185 #[test]
186 fn empty_file_returns_none() {
187 let _g = ENV_LOCK.lock().unwrap();
188 let dir = tempfile::tempdir().unwrap();
189 let path = dir.path().join("tok");
190 std::fs::write(&path, " \n\t").unwrap();
191 let _e = EnvGuard::set(&[
192 ("ATI_SESSION_TOKEN", None),
193 ("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
194 ]);
195 assert_eq!(resolve_session_token().unwrap(), None);
196 }
197
198 #[test]
199 fn missing_file_no_env_returns_none() {
200 let _g = ENV_LOCK.lock().unwrap();
201 let _e = EnvGuard::set(&[
202 ("ATI_SESSION_TOKEN", None),
203 (
204 "ATI_SESSION_TOKEN_FILE",
205 Some("/nonexistent/path/never/exists/session_token"),
206 ),
207 ]);
208 assert_eq!(resolve_session_token().unwrap(), None);
209 }
210
211 #[cfg(unix)]
212 #[test]
213 fn unreadable_file_returns_err_with_path() {
214 use std::os::unix::fs::PermissionsExt;
215
216 if unsafe { libc::geteuid() } == 0 {
219 eprintln!("skipping unreadable_file_returns_err_with_path: running as root");
220 return;
221 }
222
223 let _g = ENV_LOCK.lock().unwrap();
224 let dir = tempfile::tempdir().unwrap();
225 let path = dir.path().join("tok");
226 std::fs::write(&path, "secret").unwrap();
227 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
228
229 let _e = EnvGuard::set(&[
230 ("ATI_SESSION_TOKEN", None),
231 ("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
232 ]);
233 let err = resolve_session_token().unwrap_err();
234 assert!(err.contains("Cannot read"), "unexpected error: {err}");
235 assert!(
236 err.contains(path.to_str().unwrap()),
237 "error should mention path: {err}"
238 );
239
240 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
242 }
243
244 #[test]
258 fn resolve_token_reads_arbitrary_env_var() {
259 let _g = ENV_LOCK.lock().unwrap();
260 let _e = EnvGuard::set(&[("PARCHA_TOOLS_SESSION_TOKEN", Some("parcha-tok"))]);
261 assert_eq!(
262 resolve_token("PARCHA_TOOLS_SESSION_TOKEN").unwrap(),
263 Some("parcha-tok".to_string())
264 );
265 }
266
267 #[test]
268 fn resolve_token_falls_back_to_named_file_env() {
269 let _g = ENV_LOCK.lock().unwrap();
270 let dir = tempfile::tempdir().unwrap();
271 let path = dir.path().join("ptok");
272 std::fs::write(&path, "file-tok").unwrap();
273 let _e = EnvGuard::set(&[
274 ("PARCHA_TOOLS_SESSION_TOKEN", None),
275 (
276 "PARCHA_TOOLS_SESSION_TOKEN_FILE",
277 Some(path.to_str().unwrap()),
278 ),
279 ]);
280 assert_eq!(
281 resolve_token("PARCHA_TOOLS_SESSION_TOKEN").unwrap(),
282 Some("file-tok".to_string())
283 );
284 }
285
286 #[test]
287 fn resolve_token_empty_env_falls_through_to_file() {
288 let _g = ENV_LOCK.lock().unwrap();
289 let dir = tempfile::tempdir().unwrap();
290 let path = dir.path().join("ptok");
291 std::fs::write(&path, "from-file").unwrap();
292 let _e = EnvGuard::set(&[
293 ("PARCHA_TOOLS_SESSION_TOKEN", Some("")),
294 (
295 "PARCHA_TOOLS_SESSION_TOKEN_FILE",
296 Some(path.to_str().unwrap()),
297 ),
298 ]);
299 assert_eq!(
300 resolve_token("PARCHA_TOOLS_SESSION_TOKEN").unwrap(),
301 Some("from-file".to_string())
302 );
303 }
304
305 #[test]
306 fn resolve_session_token_wrapper_back_compat() {
307 let _g = ENV_LOCK.lock().unwrap();
311 let _e = EnvGuard::set(&[("ATI_SESSION_TOKEN", Some("wrapped-tok"))]);
312 assert_eq!(
313 resolve_session_token().unwrap(),
314 resolve_token("ATI_SESSION_TOKEN").unwrap(),
315 );
316 assert_eq!(
317 resolve_session_token().unwrap(),
318 Some("wrapped-tok".to_string())
319 );
320 }
321
322 #[test]
323 fn default_token_file_hardcoded_ati_session_token() {
324 assert_eq!(
327 default_token_file("ATI_SESSION_TOKEN"),
328 "/run/ati/session_token"
329 );
330 }
331
332 #[test]
333 fn default_token_file_strips_session_token_suffix() {
334 assert_eq!(
335 default_token_file("PARCHA_TOOLS_SESSION_TOKEN"),
336 "/run/ati/parcha_tools"
337 );
338 assert_eq!(
339 default_token_file("FOO_BAR_SESSION_TOKEN"),
340 "/run/ati/foo_bar"
341 );
342 }
343
344 #[test]
345 fn default_token_file_lowercases_when_no_suffix_to_strip() {
346 assert_eq!(default_token_file("CUSTOM_TOKEN"), "/run/ati/custom_token");
347 assert_eq!(default_token_file("FOO"), "/run/ati/foo");
348 }
349}