Skip to main content

bwx/
dirs.rs

1use crate::prelude::*;
2
3use std::os::unix::fs::{DirBuilderExt as _, PermissionsExt as _};
4
5pub fn make_all() -> Result<()> {
6    create_dir_all_with_permissions(&config_dir(), 0o700)?;
7    create_dir_all_with_permissions(&cache_dir(), 0o700)?;
8    create_dir_all_with_permissions(&runtime_dir(), 0o700)?;
9    create_dir_all_with_permissions(&data_dir(), 0o700)?;
10
11    Ok(())
12}
13
14fn create_dir_all_with_permissions(
15    path: &std::path::Path,
16    mode: u32,
17) -> Result<()> {
18    // create with the correct mode to avoid a race between mkdir and chmod
19    std::fs::DirBuilder::new()
20        .recursive(true)
21        .mode(mode)
22        .create(path)
23        .map_err(|source| Error::CreateDirectory {
24            source,
25            file: path.to_path_buf(),
26        })?;
27    // forcibly set the mode in case the directory already existed
28    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
29        .map_err(|source| Error::CreateDirectory {
30            source,
31            file: path.to_path_buf(),
32        })?;
33    Ok(())
34}
35
36pub fn config_file() -> std::path::PathBuf {
37    config_dir().join("config.json")
38}
39
40pub fn db_file(server: &str, email: &str) -> std::path::PathBuf {
41    let server = urlencoding::encode(server).into_owned();
42    cache_dir().join(format!("{server}:{email}.json"))
43}
44
45pub fn pid_file() -> std::path::PathBuf {
46    runtime_dir().join("pidfile")
47}
48
49pub fn agent_stdout_file() -> std::path::PathBuf {
50    data_dir().join("agent.out")
51}
52
53pub fn agent_stderr_file() -> std::path::PathBuf {
54    data_dir().join("agent.err")
55}
56
57pub fn device_id_file() -> std::path::PathBuf {
58    data_dir().join("device_id")
59}
60
61pub fn socket_file() -> std::path::PathBuf {
62    runtime_dir().join("socket")
63}
64
65pub fn ssh_agent_socket_file() -> std::path::PathBuf {
66    runtime_dir().join("ssh-agent-socket")
67}
68
69fn home_dir() -> std::path::PathBuf {
70    std::env::var_os("HOME").map_or_else(
71        || std::path::PathBuf::from("/"),
72        std::path::PathBuf::from,
73    )
74}
75
76#[cfg(target_os = "macos")]
77fn config_dir() -> std::path::PathBuf {
78    home_dir()
79        .join("Library/Application Support")
80        .join(profile())
81}
82
83#[cfg(target_os = "macos")]
84fn cache_dir() -> std::path::PathBuf {
85    home_dir().join("Library/Caches").join(profile())
86}
87
88#[cfg(target_os = "macos")]
89fn data_dir() -> std::path::PathBuf {
90    config_dir()
91}
92
93#[cfg(not(target_os = "macos"))]
94fn xdg_or(env: &str, fallback_rel: &str) -> std::path::PathBuf {
95    std::env::var_os(env)
96        .filter(|v| std::path::Path::new(v).is_absolute())
97        .map_or_else(
98            || home_dir().join(fallback_rel),
99            std::path::PathBuf::from,
100        )
101}
102
103#[cfg(not(target_os = "macos"))]
104fn config_dir() -> std::path::PathBuf {
105    xdg_or("XDG_CONFIG_HOME", ".config").join(profile())
106}
107
108#[cfg(not(target_os = "macos"))]
109fn cache_dir() -> std::path::PathBuf {
110    xdg_or("XDG_CACHE_HOME", ".cache").join(profile())
111}
112
113#[cfg(not(target_os = "macos"))]
114fn data_dir() -> std::path::PathBuf {
115    xdg_or("XDG_DATA_HOME", ".local/share").join(profile())
116}
117
118fn runtime_dir() -> std::path::PathBuf {
119    // Honor XDG_RUNTIME_DIR on all platforms when explicitly set. macOS has
120    // no native equivalent, but respecting the override lets tests and
121    // advanced users isolate per-instance sockets. Falls through to a
122    // $TMPDIR-based path when unset.
123    if let Some(d) = std::env::var_os("XDG_RUNTIME_DIR") {
124        if std::path::Path::new(&d).is_absolute() {
125            return std::path::PathBuf::from(d).join(profile());
126        }
127    }
128    format!(
129        "{}/{}-{}",
130        std::env::temp_dir().to_string_lossy(),
131        profile(),
132        rustix::process::getuid().as_raw()
133    )
134    .into()
135}
136
137pub fn profile() -> String {
138    match std::env::var("BWX_PROFILE") {
139        Ok(profile) if !profile.is_empty() => format!("bwx-{profile}"),
140        _ => "bwx".to_string(),
141    }
142}