use std::io::ErrorKind;
const DEFAULT_SESSION_TOKEN_FILE: &str = "/run/ati/session_token";
pub(crate) fn default_token_file(env_name: &str) -> String {
if env_name == "ATI_SESSION_TOKEN" {
return DEFAULT_SESSION_TOKEN_FILE.to_string();
}
let trimmed = env_name
.strip_suffix("_SESSION_TOKEN")
.or_else(|| env_name.strip_suffix("_session_token"))
.unwrap_or(env_name);
format!("/run/ati/{}", trimmed.to_lowercase())
}
pub fn resolve_token(env_name: &str) -> Result<Option<String>, String> {
if let Ok(raw) = std::env::var(env_name) {
let trimmed = raw.trim();
if !trimmed.is_empty() {
return Ok(Some(trimmed.to_string()));
}
}
let file_env = format!("{env_name}_FILE");
let path = std::env::var(&file_env)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| default_token_file(env_name));
match std::fs::read_to_string(&path) {
Ok(contents) => {
let trimmed = contents.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("Cannot read {path}: {e}")),
}
}
pub fn resolve_session_token() -> Result<Option<String>, String> {
resolve_token("ATI_SESSION_TOKEN")
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard {
keys: Vec<&'static str>,
prev: Vec<(String, Option<String>)>,
}
impl EnvGuard {
fn set(pairs: &[(&'static str, Option<&str>)]) -> Self {
let mut prev = Vec::new();
let mut keys = Vec::new();
for (k, v) in pairs {
prev.push(((*k).to_string(), std::env::var(k).ok()));
keys.push(*k);
match v {
Some(val) => std::env::set_var(k, val),
None => std::env::remove_var(k),
}
}
Self { keys, prev }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (k, v) in &self.prev {
match v {
Some(val) => std::env::set_var(k, val),
None => std::env::remove_var(k),
}
}
let _ = &self.keys;
}
}
#[test]
fn env_var_wins_over_file() {
let _g = ENV_LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tok");
std::fs::write(&path, "from-file").unwrap();
let _e = EnvGuard::set(&[
("ATI_SESSION_TOKEN", Some("from-env")),
("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
]);
assert_eq!(
resolve_session_token().unwrap(),
Some("from-env".to_string())
);
}
#[test]
fn empty_env_falls_through_to_file_and_rereads() {
let _g = ENV_LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tok");
std::fs::write(&path, "tok-v1").unwrap();
let _e = EnvGuard::set(&[
("ATI_SESSION_TOKEN", Some("")),
("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
]);
assert_eq!(resolve_session_token().unwrap(), Some("tok-v1".to_string()));
std::fs::write(&path, "tok-v2").unwrap();
assert_eq!(resolve_session_token().unwrap(), Some("tok-v2".to_string()));
}
#[test]
fn trims_whitespace_in_file_contents() {
let _g = ENV_LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tok");
std::fs::write(&path, " hello-tok\n\n").unwrap();
let _e = EnvGuard::set(&[
("ATI_SESSION_TOKEN", None),
("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
]);
assert_eq!(
resolve_session_token().unwrap(),
Some("hello-tok".to_string())
);
}
#[test]
fn empty_file_returns_none() {
let _g = ENV_LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tok");
std::fs::write(&path, " \n\t").unwrap();
let _e = EnvGuard::set(&[
("ATI_SESSION_TOKEN", None),
("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
]);
assert_eq!(resolve_session_token().unwrap(), None);
}
#[test]
fn missing_file_no_env_returns_none() {
let _g = ENV_LOCK.lock().unwrap();
let _e = EnvGuard::set(&[
("ATI_SESSION_TOKEN", None),
(
"ATI_SESSION_TOKEN_FILE",
Some("/nonexistent/path/never/exists/session_token"),
),
]);
assert_eq!(resolve_session_token().unwrap(), None);
}
#[cfg(unix)]
#[test]
fn unreadable_file_returns_err_with_path() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipping unreadable_file_returns_err_with_path: running as root");
return;
}
let _g = ENV_LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tok");
std::fs::write(&path, "secret").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
let _e = EnvGuard::set(&[
("ATI_SESSION_TOKEN", None),
("ATI_SESSION_TOKEN_FILE", Some(path.to_str().unwrap())),
]);
let err = resolve_session_token().unwrap_err();
assert!(err.contains("Cannot read"), "unexpected error: {err}");
assert!(
err.contains(path.to_str().unwrap()),
"error should mention path: {err}"
);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[test]
fn resolve_token_reads_arbitrary_env_var() {
let _g = ENV_LOCK.lock().unwrap();
let _e = EnvGuard::set(&[("PARCHA_TOOLS_SESSION_TOKEN", Some("parcha-tok"))]);
assert_eq!(
resolve_token("PARCHA_TOOLS_SESSION_TOKEN").unwrap(),
Some("parcha-tok".to_string())
);
}
#[test]
fn resolve_token_falls_back_to_named_file_env() {
let _g = ENV_LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ptok");
std::fs::write(&path, "file-tok").unwrap();
let _e = EnvGuard::set(&[
("PARCHA_TOOLS_SESSION_TOKEN", None),
(
"PARCHA_TOOLS_SESSION_TOKEN_FILE",
Some(path.to_str().unwrap()),
),
]);
assert_eq!(
resolve_token("PARCHA_TOOLS_SESSION_TOKEN").unwrap(),
Some("file-tok".to_string())
);
}
#[test]
fn resolve_token_empty_env_falls_through_to_file() {
let _g = ENV_LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ptok");
std::fs::write(&path, "from-file").unwrap();
let _e = EnvGuard::set(&[
("PARCHA_TOOLS_SESSION_TOKEN", Some("")),
(
"PARCHA_TOOLS_SESSION_TOKEN_FILE",
Some(path.to_str().unwrap()),
),
]);
assert_eq!(
resolve_token("PARCHA_TOOLS_SESSION_TOKEN").unwrap(),
Some("from-file".to_string())
);
}
#[test]
fn resolve_session_token_wrapper_back_compat() {
let _g = ENV_LOCK.lock().unwrap();
let _e = EnvGuard::set(&[("ATI_SESSION_TOKEN", Some("wrapped-tok"))]);
assert_eq!(
resolve_session_token().unwrap(),
resolve_token("ATI_SESSION_TOKEN").unwrap(),
);
assert_eq!(
resolve_session_token().unwrap(),
Some("wrapped-tok".to_string())
);
}
#[test]
fn default_token_file_hardcoded_ati_session_token() {
assert_eq!(
default_token_file("ATI_SESSION_TOKEN"),
"/run/ati/session_token"
);
}
#[test]
fn default_token_file_strips_session_token_suffix() {
assert_eq!(
default_token_file("PARCHA_TOOLS_SESSION_TOKEN"),
"/run/ati/parcha_tools"
);
assert_eq!(
default_token_file("FOO_BAR_SESSION_TOKEN"),
"/run/ati/foo_bar"
);
}
#[test]
fn default_token_file_lowercases_when_no_suffix_to_strip() {
assert_eq!(default_token_file("CUSTOM_TOKEN"), "/run/ati/custom_token");
assert_eq!(default_token_file("FOO"), "/run/ati/foo");
}
}