Skip to main content

bear_cli/
config.rs

1use std::env;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result, anyhow};
6use base64::Engine as _;
7use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
8
9const BEAR_GROUP_CONTAINER_SUFFIX: &str = ".net.shinyfrog.bear";
10const BEAR_DATABASE_SUFFIX: &str = "Application Data/database.sqlite";
11
12pub fn expand_tilde(path: &str) -> Result<PathBuf> {
13    if let Some(rest) = path.strip_prefix("~/") {
14        let home = env::var_os("HOME").ok_or_else(|| anyhow!("$HOME is not set"))?;
15        return Ok(PathBuf::from(home).join(rest));
16    }
17
18    Ok(PathBuf::from(path))
19}
20
21pub fn app_support_dir() -> Result<PathBuf> {
22    let home = env::var_os("HOME").ok_or_else(|| anyhow!("$HOME is not set"))?;
23    Ok(PathBuf::from(home)
24        .join("Library")
25        .join("Application Support")
26        .join("bear-cli"))
27}
28
29pub fn resolve_database_path(override_path: Option<&str>) -> Result<PathBuf> {
30    if let Some(path) = override_path {
31        return expand_tilde(path);
32    }
33
34    let home = env::var_os("HOME").ok_or_else(|| anyhow!("$HOME is not set"))?;
35    let group_containers = PathBuf::from(home).join("Library").join("Group Containers");
36
37    find_bear_database_in(&group_containers)
38}
39
40fn find_bear_database_in(group_containers: &Path) -> Result<PathBuf> {
41    let entries = fs::read_dir(group_containers).with_context(|| {
42        format!(
43            "failed to read Bear group containers from {}",
44            group_containers.display()
45        )
46    })?;
47
48    let mut matches = entries
49        .filter_map(|entry| entry.ok())
50        .map(|entry| entry.path())
51        .filter(|path| path.is_dir())
52        .filter(|path| {
53            path.file_name()
54                .and_then(|name| name.to_str())
55                .is_some_and(|name| name.ends_with(BEAR_GROUP_CONTAINER_SUFFIX))
56        })
57        .map(|path| path.join(BEAR_DATABASE_SUFFIX))
58        .filter(|path| path.is_file())
59        .collect::<Vec<_>>();
60
61    matches.sort();
62
63    matches.into_iter().next().ok_or_else(|| {
64        anyhow!(
65            "could not locate Bear database under {} matching *{}",
66            group_containers.display(),
67            BEAR_GROUP_CONTAINER_SUFFIX
68        )
69    })
70}
71
72pub fn token_path() -> Result<PathBuf> {
73    Ok(app_support_dir()?.join("token"))
74}
75
76pub fn save_token(token: &str) -> Result<()> {
77    let dir = app_support_dir()?;
78    fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
79    fs::write(token_path()?, format!("{token}\n")).context("failed to write token file")?;
80    Ok(())
81}
82
83pub fn load_token() -> Result<Option<String>> {
84    let path = token_path()?;
85    match fs::read_to_string(&path) {
86        Ok(contents) => Ok(Some(contents.trim().to_string())),
87        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
88        Err(err) => Err(err).with_context(|| format!("failed to read {}", path.display())),
89    }
90}
91
92pub fn encode_file(path: &Path) -> Result<String> {
93    let bytes = fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
94    Ok(BASE64_STANDARD.encode(bytes))
95}
96
97#[cfg(test)]
98mod tests {
99    use std::fs;
100
101    use super::{expand_tilde, find_bear_database_in};
102
103    #[test]
104    fn expands_tilde() {
105        let expanded = expand_tilde("~/tmp").expect("tilde should expand");
106        assert!(expanded.to_string_lossy().ends_with("/tmp"));
107    }
108
109    #[test]
110    fn finds_database_dynamically() {
111        let base =
112            std::env::temp_dir().join(format!("bear-cli-config-test-{}", std::process::id()));
113        let _ = fs::remove_dir_all(&base);
114        let group = base.join("ABC123.net.shinyfrog.bear");
115        let database = group.join("Application Data").join("database.sqlite");
116        fs::create_dir_all(database.parent().expect("database parent"))
117            .expect("test directories should be created");
118        fs::write(&database, b"").expect("database file should be created");
119
120        let found = find_bear_database_in(&base).expect("database should be discovered");
121        assert_eq!(found, database);
122
123        let _ = fs::remove_dir_all(&base);
124    }
125}