ballistics_engine/
credentials.rs1use std::fs;
8use std::io;
9use std::path::{Path, PathBuf};
10
11const ENV_VAR: &str = "BALLISTICS_API_TOKEN";
12
13pub fn credentials_path() -> Option<PathBuf> {
15 dirs::home_dir().map(|home| home.join(".ballistics").join("credentials.toml"))
16}
17
18fn save_token_at(path: &Path, token: &str) -> io::Result<()> {
19 if let Some(dir) = path.parent() {
20 fs::create_dir_all(dir)?;
21 }
22 fs::write(path, format!("token = \"{}\"\n", token.trim()))?;
23 #[cfg(unix)]
24 {
25 use std::os::unix::fs::PermissionsExt;
26 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
27 }
28 Ok(())
29}
30
31fn load_token_from(path: &Path) -> Option<String> {
32 let content = fs::read_to_string(path).ok()?;
33 for line in content.lines() {
34 let line = line.trim();
35 if let Some(rest) = line.strip_prefix("token") {
36 let rest = rest.trim_start().strip_prefix('=')?.trim();
37 let val = rest.trim_matches('"');
38 if !val.is_empty() {
39 return Some(val.to_string());
40 }
41 }
42 }
43 None
44}
45
46pub fn save_token(token: &str) -> io::Result<()> {
48 let path = credentials_path()
49 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no home directory"))?;
50 save_token_at(&path, token)
51}
52
53pub fn load_token() -> Option<String> {
55 if let Ok(t) = std::env::var(ENV_VAR) {
56 let t = t.trim().to_string();
57 if !t.is_empty() {
58 return Some(t);
59 }
60 }
61 load_token_from(&credentials_path()?)
62}
63
64pub fn clear_token() -> io::Result<()> {
66 if let Some(path) = credentials_path() {
67 if path.exists() {
68 fs::remove_file(path)?;
69 }
70 }
71 Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn temp_path(tag: &str) -> PathBuf {
79 std::env::temp_dir()
80 .join(format!("ballistics-cred-{}-{}", tag, std::process::id()))
81 .join("credentials.toml")
82 }
83
84 #[test]
85 fn save_and_load_roundtrip() {
86 let path = temp_path("rt");
87 let _ = fs::remove_dir_all(path.parent().unwrap());
88 save_token_at(&path, "bpat_abc123").unwrap();
89 assert_eq!(load_token_from(&path).as_deref(), Some("bpat_abc123"));
90 let _ = fs::remove_dir_all(path.parent().unwrap());
91 }
92
93 #[test]
94 fn load_from_missing_file_is_none() {
95 let path = temp_path("missing");
96 let _ = fs::remove_dir_all(path.parent().unwrap());
97 assert!(load_token_from(&path).is_none());
98 }
99
100 #[test]
101 fn env_var_takes_precedence_over_file() {
102 std::env::set_var(ENV_VAR, "bpat_env");
103 assert_eq!(load_token().as_deref(), Some("bpat_env"));
104 std::env::remove_var(ENV_VAR);
105 }
106
107 #[cfg(unix)]
108 #[test]
109 fn saved_file_is_0600() {
110 use std::os::unix::fs::PermissionsExt;
111 let path = temp_path("perm");
112 let _ = fs::remove_dir_all(path.parent().unwrap());
113 save_token_at(&path, "bpat_x").unwrap();
114 let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
115 assert_eq!(mode, 0o600);
116 let _ = fs::remove_dir_all(path.parent().unwrap());
117 }
118}