Skip to main content

lust/packages/
credentials.rs

1use dirs::home_dir;
2use std::{
3    fs,
4    io::{self, Read, Write},
5    path::PathBuf,
6};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum CredentialsError {
11    #[error("unable to determine user home directory")]
12    HomeDirUnavailable,
13
14    #[error("failed to access credentials at {path}: {source}")]
15    Io {
16        path: PathBuf,
17        #[source]
18        source: io::Error,
19    },
20}
21
22#[derive(Debug, Clone)]
23pub struct Credentials {
24    token: String,
25}
26
27impl Credentials {
28    pub fn new(token: impl Into<String>) -> Self {
29        Self {
30            token: token.into(),
31        }
32    }
33
34    pub fn token(&self) -> &str {
35        &self.token
36    }
37}
38
39pub fn credentials_file() -> Result<PathBuf, CredentialsError> {
40    let home = home_dir().ok_or(CredentialsError::HomeDirUnavailable)?;
41    let dir = home.join(".lust");
42    Ok(dir.join("credentials"))
43}
44
45pub fn load_credentials() -> Result<Option<Credentials>, CredentialsError> {
46    let path = credentials_file()?;
47    match fs::File::open(&path) {
48        Ok(mut file) => {
49            let mut buf = String::new();
50            file.read_to_string(&mut buf)
51                .map_err(|source| CredentialsError::Io {
52                    path: path.clone(),
53                    source,
54                })?;
55            let token = buf.trim().to_string();
56            if token.is_empty() {
57                Ok(None)
58            } else {
59                Ok(Some(Credentials::new(token)))
60            }
61        }
62        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
63        Err(source) => Err(CredentialsError::Io { path, source }),
64    }
65}
66
67pub fn save_credentials(token: &str) -> Result<(), CredentialsError> {
68    let path = credentials_file()?;
69    if let Some(parent) = path.parent() {
70        fs::create_dir_all(parent).map_err(|source| CredentialsError::Io {
71            path: parent.to_path_buf(),
72            source,
73        })?;
74    }
75    let mut file = fs::File::create(&path).map_err(|source| CredentialsError::Io {
76        path: path.clone(),
77        source,
78    })?;
79    file.write_all(token.as_bytes())
80        .and_then(|_| file.write_all(b"\n"))
81        .map_err(|source| CredentialsError::Io { path, source })
82}
83
84pub fn clear_credentials() -> Result<(), CredentialsError> {
85    let path = credentials_file()?;
86    match fs::remove_file(&path) {
87        Ok(()) => Ok(()),
88        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
89        Err(source) => Err(CredentialsError::Io { path, source }),
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::env;
97    use tempfile::tempdir;
98
99    #[test]
100    fn save_and_load_credentials() {
101        let dir = tempdir().unwrap();
102        let original_home = env::var("HOME").ok();
103        let original_userprofile = env::var("USERPROFILE").ok();
104        env::set_var("HOME", dir.path());
105        env::set_var("USERPROFILE", dir.path());
106
107        save_credentials("secret-token").unwrap();
108        let creds = load_credentials().unwrap().unwrap();
109        assert_eq!(creds.token(), "secret-token");
110
111        let path = credentials_file().unwrap();
112        assert!(path.exists());
113
114        env::remove_var("HOME");
115        env::remove_var("USERPROFILE");
116        if let Some(home) = original_home {
117            env::set_var("HOME", home);
118        }
119        if let Some(userprofile) = original_userprofile {
120            env::set_var("USERPROFILE", userprofile);
121        }
122    }
123}