1use std::path::Path;
2
3pub fn generate_token() -> String {
4 use base64::Engine;
5 use rand::RngExt;
6 let mut bytes = [0u8; 32];
7 rand::rng().fill(&mut bytes);
8 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
9}
10
11pub fn write_token(path: &Path, token: &str) -> anyhow::Result<()> {
12 if let Some(parent) = path.parent() {
13 std::fs::create_dir_all(parent)?;
14 }
15 std::fs::write(path, token)?;
16 #[cfg(unix)]
17 {
18 use std::os::unix::fs::PermissionsExt;
19 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
20 }
21 Ok(())
22}
23
24pub fn read_token(path: &Path) -> anyhow::Result<String> {
25 let content = std::fs::read_to_string(path)?;
26 Ok(content.trim().to_string())
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32 use std::fs;
33
34 #[test]
35 fn test_generate_token_is_43_chars_base64url() {
36 let token = generate_token();
37 assert_eq!(token.len(), 43, "token should be 43 base64url chars");
38 assert!(
39 token
40 .chars()
41 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
42 "token should only contain base64url chars: {token}"
43 );
44 }
45
46 #[test]
47 fn test_generate_token_is_unique() {
48 let t1 = generate_token();
49 let t2 = generate_token();
50 assert_ne!(t1, t2, "two generated tokens should differ");
51 }
52
53 #[test]
54 fn test_write_and_read_token_roundtrip() {
55 let dir = tempfile::tempdir().unwrap();
56 let path = dir.path().join("test-token");
57 let token = "test-token-value-abc123";
58 write_token(&path, token).unwrap();
59 let read_back = read_token(&path).unwrap();
60 assert_eq!(read_back, token);
61 }
62
63 #[test]
64 fn test_write_token_creates_parent_dirs() {
65 let dir = tempfile::tempdir().unwrap();
66 let path = dir.path().join("nested").join("dir").join("token");
67 let token = "abc";
68 write_token(&path, token).unwrap();
69 assert!(path.exists());
70 assert_eq!(read_token(&path).unwrap(), token);
71 }
72
73 #[test]
74 fn test_read_token_trims_whitespace() {
75 let dir = tempfile::tempdir().unwrap();
76 let path = dir.path().join("token");
77 fs::write(&path, " my-token-value \n").unwrap();
78 let token = read_token(&path).unwrap();
79 assert_eq!(token, "my-token-value");
80 }
81
82 #[test]
83 fn test_read_token_missing_file_errors() {
84 let result = read_token(Path::new("/nonexistent/path/token"));
85 assert!(result.is_err());
86 }
87
88 #[cfg(unix)]
89 #[test]
90 fn test_write_token_sets_0600_permissions() {
91 use std::os::unix::fs::PermissionsExt;
92 let dir = tempfile::tempdir().unwrap();
93 let path = dir.path().join("token");
94 write_token(&path, "secret").unwrap();
95 let perms = fs::metadata(&path).unwrap().permissions();
96 assert_eq!(perms.mode() & 0o777, 0o600, "token file should be 0600");
97 }
98}