Skip to main content

starla_common/
paths.rs

1//! Path resolution for Starla
2//!
3//! Directory resolution priority:
4//!
5//! **Config directory** (for config.toml):
6//! 1. `$CONFIGURATION_DIRECTORY` (systemd)
7//! 2. Container: `/config`
8//! 3. `$XDG_CONFIG_HOME/starla`
9//! 4. Root: `/etc/starla`, non-root: `~/.config/starla`
10//!
11//! **State directory** (for keys, probe_id, known_hosts):
12//! 1. CLI `--state-dir` (via override)
13//! 2. `$STATE_DIRECTORY` (systemd)
14//! 3. Container: `/state`
15//! 4. `$XDG_STATE_HOME/starla`
16//! 5. Root: `/var/lib/starla`, non-root: `~/.local/state/starla`
17//!
18//! **Runtime directory** (for ephemeral databases, caches):
19//! 1. `$RUNTIME_DIRECTORY` (systemd)
20//! 2. Container: `/run/starla`
21//! 3. `$XDG_RUNTIME_DIR/starla`
22//! 4. Root: `/run/starla`, non-root: `/tmp/starla-<uid>`
23
24use std::path::PathBuf;
25use std::sync::OnceLock;
26
27/// Application name used in subdirectories
28const APP_NAME: &str = "starla";
29
30/// Detect if running inside a container (Docker, Podman, etc.)
31///
32/// Checks for:
33/// - `container` env var (set by podman, systemd-nspawn)
34/// - `/.dockerenv` file (Docker)
35/// - `/run/.containerenv` file (Podman)
36fn is_container() -> bool {
37    std::env::var("container").is_ok()
38        || std::path::Path::new("/.dockerenv").exists()
39        || std::path::Path::new("/run/.containerenv").exists()
40}
41
42/// CLI override for state directory (set once at startup via `set_state_dir`)
43static STATE_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
44
45/// CLI override for runtime directory (set once at startup via
46/// `set_runtime_dir`)
47static RUNTIME_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
48
49/// Set the state directory override (from `--state-dir` CLI arg).
50/// Must be called before any `state_dir()` calls. Subsequent calls are ignored.
51pub fn set_state_dir(path: PathBuf) {
52    let _ = STATE_DIR_OVERRIDE.set(path);
53}
54
55/// Set the runtime directory override (from `--runtime-dir` CLI arg).
56/// Must be called before any `runtime_dir()` calls. Subsequent calls are
57/// ignored.
58pub fn set_runtime_dir(path: PathBuf) {
59    let _ = RUNTIME_DIR_OVERRIDE.set(path);
60}
61
62/// Get the configuration directory path
63///
64/// Priority:
65/// 1. `$CONFIGURATION_DIRECTORY` (systemd)
66/// 2. Container: `/config`
67/// 3. `$XDG_CONFIG_HOME/starla`
68/// 4. Root: `/etc/starla`, non-root: `~/.config/starla`
69pub fn config_dir() -> PathBuf {
70    if let Ok(dir) = std::env::var("CONFIGURATION_DIRECTORY") {
71        return PathBuf::from(dir);
72    }
73
74    if is_container() {
75        return PathBuf::from("/config");
76    }
77
78    if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
79        return PathBuf::from(xdg_config).join(APP_NAME);
80    }
81
82    if is_root() {
83        return PathBuf::from("/etc").join(APP_NAME);
84    }
85
86    if let Some(home) = home_dir() {
87        return home.join(".config").join(APP_NAME);
88    }
89
90    PathBuf::from("/etc").join(APP_NAME)
91}
92
93/// Get the state directory path (for databases, keys, etc.)
94///
95/// Priority:
96/// 1. CLI override (set via `set_state_dir`)
97/// 2. `$STATE_DIRECTORY` (systemd)
98/// 3. Container: `/state`
99/// 4. `$XDG_STATE_HOME/starla`
100/// 5. Root: `/var/lib/starla`, non-root: `~/.local/state/starla`
101pub fn state_dir() -> PathBuf {
102    if let Some(override_dir) = STATE_DIR_OVERRIDE.get() {
103        return override_dir.clone();
104    }
105
106    if let Ok(dir) = std::env::var("STATE_DIRECTORY") {
107        return PathBuf::from(dir);
108    }
109
110    if is_container() {
111        return PathBuf::from("/state");
112    }
113
114    if let Ok(xdg_state) = std::env::var("XDG_STATE_HOME") {
115        return PathBuf::from(xdg_state).join(APP_NAME);
116    }
117
118    if is_root() {
119        return PathBuf::from("/var/lib").join(APP_NAME);
120    }
121
122    if let Some(home) = home_dir() {
123        return home.join(".local").join("state").join(APP_NAME);
124    }
125
126    PathBuf::from("/var/lib").join(APP_NAME)
127}
128
129/// Get the runtime directory path (for ephemeral data: databases, caches)
130///
131/// Priority:
132/// 1. CLI override (set via `set_runtime_dir`)
133/// 2. `$RUNTIME_DIRECTORY` (systemd)
134/// 3. Container: `/run/starla`
135/// 4. `$XDG_RUNTIME_DIR/starla`
136/// 5. Root: `/run/starla`, non-root: `/tmp/starla-<uid>`
137pub fn runtime_dir() -> PathBuf {
138    if let Some(override_dir) = RUNTIME_DIR_OVERRIDE.get() {
139        return override_dir.clone();
140    }
141
142    if let Ok(dir) = std::env::var("RUNTIME_DIRECTORY") {
143        return PathBuf::from(dir);
144    }
145
146    if is_container() {
147        return PathBuf::from("/run").join(APP_NAME);
148    }
149
150    if let Ok(xdg_runtime) = std::env::var("XDG_RUNTIME_DIR") {
151        return PathBuf::from(xdg_runtime).join(APP_NAME);
152    }
153
154    if is_root() {
155        return PathBuf::from("/run").join(APP_NAME);
156    }
157
158    // Per-user temp directory
159    let uid = {
160        #[cfg(unix)]
161        {
162            unsafe { libc::getuid() }
163        }
164        #[cfg(not(unix))]
165        {
166            0u32
167        }
168    };
169    std::env::temp_dir().join(format!("{}-{}", APP_NAME, uid))
170}
171
172/// Get the default config file path
173pub fn config_file() -> PathBuf {
174    config_dir().join("config.toml")
175}
176
177/// Get the default probe key path
178pub fn probe_key_path() -> PathBuf {
179    state_dir().join("probe_key")
180}
181
182/// Get the default probe public key path
183pub fn probe_pubkey_path() -> PathBuf {
184    state_dir().join("probe_key.pub")
185}
186
187/// Get the known SSH host keys path
188pub fn known_hosts_path() -> PathBuf {
189    state_dir().join("known_hosts")
190}
191
192/// Get the status socket path (for tray app communication)
193pub fn status_socket_path() -> PathBuf {
194    runtime_dir().join("starla.sock")
195}
196
197/// Get the probe ID file path
198pub fn probe_id_path() -> PathBuf {
199    state_dir().join("probe_id")
200}
201
202/// Read the probe ID from the state directory
203///
204/// Returns None if the file doesn't exist or can't be parsed
205pub fn read_probe_id() -> Option<u32> {
206    let path = probe_id_path();
207    match std::fs::read_to_string(&path) {
208        Ok(content) => content.trim().parse().ok(),
209        Err(_) => None,
210    }
211}
212
213/// Write the probe ID to the state directory
214///
215/// Creates the state directory if it doesn't exist
216pub fn write_probe_id(probe_id: u32) -> std::io::Result<()> {
217    let dir = state_dir();
218    ensure_dir(&dir)?;
219    let path = probe_id_path();
220    std::fs::write(&path, probe_id.to_string())
221}
222
223/// Check if the current process is running as root (UID 0)
224#[cfg(unix)]
225fn is_root() -> bool {
226    // Safety: getuid() is a simple syscall with no safety concerns
227    unsafe { libc::getuid() == 0 }
228}
229
230#[cfg(not(unix))]
231fn is_root() -> bool {
232    false
233}
234
235/// Get the user's home directory
236fn home_dir() -> Option<PathBuf> {
237    if let Ok(home) = std::env::var("HOME") {
238        return Some(PathBuf::from(home));
239    }
240
241    #[cfg(windows)]
242    if let Ok(home) = std::env::var("USERPROFILE") {
243        return Some(PathBuf::from(home));
244    }
245
246    None
247}
248
249/// Ensure a directory exists, creating it if necessary
250pub fn ensure_dir(path: &PathBuf) -> std::io::Result<()> {
251    if !path.exists() {
252        std::fs::create_dir_all(path)?;
253    }
254    Ok(())
255}
256
257/// Ensure the config directory exists
258pub fn ensure_config_dir() -> std::io::Result<PathBuf> {
259    let dir = config_dir();
260    ensure_dir(&dir)?;
261    Ok(dir)
262}
263
264/// Ensure the state directory exists
265pub fn ensure_state_dir() -> std::io::Result<PathBuf> {
266    let dir = state_dir();
267    ensure_dir(&dir)?;
268    Ok(dir)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use std::sync::Mutex;
275
276    /// Mutex to serialize tests that modify environment variables.
277    static ENV_LOCK: Mutex<()> = Mutex::new(());
278
279    #[test]
280    fn test_config_dir_with_env() {
281        let _guard = ENV_LOCK.lock().unwrap();
282
283        let original = std::env::var("CONFIGURATION_DIRECTORY").ok();
284
285        unsafe { std::env::set_var("CONFIGURATION_DIRECTORY", "/test/config") };
286        assert_eq!(config_dir(), PathBuf::from("/test/config"));
287
288        if let Some(val) = original {
289            unsafe { std::env::set_var("CONFIGURATION_DIRECTORY", val) };
290        } else {
291            unsafe { std::env::remove_var("CONFIGURATION_DIRECTORY") };
292        }
293    }
294
295    #[test]
296    fn test_state_dir_with_env() {
297        let _guard = ENV_LOCK.lock().unwrap();
298
299        let original = std::env::var("STATE_DIRECTORY").ok();
300
301        unsafe { std::env::set_var("STATE_DIRECTORY", "/test/state") };
302        assert_eq!(state_dir(), PathBuf::from("/test/state"));
303
304        if let Some(val) = original {
305            unsafe { std::env::set_var("STATE_DIRECTORY", val) };
306        } else {
307            unsafe { std::env::remove_var("STATE_DIRECTORY") };
308        }
309    }
310
311    #[test]
312    fn test_xdg_config_fallback() {
313        let _guard = ENV_LOCK.lock().unwrap();
314
315        let orig_conf_dir = std::env::var("CONFIGURATION_DIRECTORY").ok();
316        let orig_xdg = std::env::var("XDG_CONFIG_HOME").ok();
317
318        unsafe { std::env::remove_var("CONFIGURATION_DIRECTORY") };
319        unsafe { std::env::set_var("XDG_CONFIG_HOME", "/home/test/.config") };
320
321        assert_eq!(config_dir(), PathBuf::from("/home/test/.config/starla"));
322
323        if let Some(val) = orig_conf_dir {
324            unsafe { std::env::set_var("CONFIGURATION_DIRECTORY", val) };
325        }
326        if let Some(val) = orig_xdg {
327            unsafe { std::env::set_var("XDG_CONFIG_HOME", val) };
328        } else {
329            unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
330        }
331    }
332
333    #[test]
334    fn test_xdg_state_fallback() {
335        let _guard = ENV_LOCK.lock().unwrap();
336
337        let orig_state_dir = std::env::var("STATE_DIRECTORY").ok();
338        let orig_xdg = std::env::var("XDG_STATE_HOME").ok();
339
340        unsafe { std::env::remove_var("STATE_DIRECTORY") };
341        unsafe { std::env::set_var("XDG_STATE_HOME", "/home/test/.local/state") };
342
343        assert_eq!(state_dir(), PathBuf::from("/home/test/.local/state/starla"));
344
345        if let Some(val) = orig_state_dir {
346            unsafe { std::env::set_var("STATE_DIRECTORY", val) };
347        }
348        if let Some(val) = orig_xdg {
349            unsafe { std::env::set_var("XDG_STATE_HOME", val) };
350        } else {
351            unsafe { std::env::remove_var("XDG_STATE_HOME") };
352        }
353    }
354
355    #[test]
356    fn test_default_file_paths() {
357        let config = config_file();
358        assert!(config.to_string_lossy().contains("config.toml"));
359
360        let key = probe_key_path();
361        assert!(key.to_string_lossy().contains("probe_key"));
362
363        let pid = probe_id_path();
364        assert!(pid.to_string_lossy().contains("probe_id"));
365
366        let kh = known_hosts_path();
367        assert!(kh.to_string_lossy().contains("known_hosts"));
368    }
369
370    #[test]
371    fn test_probe_id_read_write() {
372        let temp_dir =
373            std::env::temp_dir().join(format!("starla-test-probe-id-{}", std::process::id()));
374        let _ = std::fs::remove_dir_all(&temp_dir);
375        std::fs::create_dir_all(&temp_dir).unwrap();
376
377        let probe_id_file = temp_dir.join("probe_id");
378        assert!(!probe_id_file.exists());
379
380        std::fs::write(&probe_id_file, "1014036").unwrap();
381
382        let content = std::fs::read_to_string(&probe_id_file).unwrap();
383        let parsed: Option<u32> = content.trim().parse().ok();
384        assert_eq!(parsed, Some(1014036));
385
386        let _ = std::fs::remove_dir_all(&temp_dir);
387    }
388}