Skip to main content

sentinelpass_protocol/
paths.rs

1//! Platform path helpers for the IPC socket and token file.
2
3use std::path::PathBuf;
4
5/// Directory holding user-level config files (IPC token, grants).
6///
7/// Mirrors `sentinelpass_core::platform::get_config_dir`; duplicated here so
8/// protocol clients do not need the core crate.
9pub fn get_config_dir() -> PathBuf {
10    let base = dirs::config_dir()
11        .or_else(dirs::data_dir)
12        .or_else(|| dirs::home_dir().map(|h| h.join(".config")))
13        .unwrap_or_else(|| PathBuf::from("."));
14
15    base.join("PasswordManager")
16}
17
18/// Owner-only runtime directory for the IPC socket (WBS-507).
19///
20/// `$XDG_RUNTIME_DIR/SentinelPass` when the runtime dir is available, else
21/// `<config dir>/runtime` (owner-only). The `/tmp` fallback is REMOVED per
22/// ADR-007: a world-traversable socket directory is exactly what the
23/// owner-only-directory requirement exists to prevent.
24pub fn default_runtime_dir() -> PathBuf {
25    if cfg!(target_os = "windows") {
26        return get_config_dir().join("runtime");
27    }
28    match std::env::var("XDG_RUNTIME_DIR") {
29        Ok(dir) if !dir.trim().is_empty() => PathBuf::from(dir).join("SentinelPass"),
30        _ => get_config_dir().join("runtime"),
31    }
32}
33
34/// Get the default IPC socket path for the platform.
35pub fn default_ipc_socket_path() -> PathBuf {
36    if cfg!(target_os = "windows") {
37        // Windows: per-user named pipe (WBS-508 hardens the server side).
38        PathBuf::from(r"\\.\pipe\SentinelPass")
39    } else {
40        default_runtime_dir().join("sentinelpass.sock")
41    }
42}
43
44/// Get the default IPC auth token path for the platform
45pub fn default_ipc_token_path() -> PathBuf {
46    get_config_dir().join("ipc.token")
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[cfg(unix)]
54    #[test]
55    fn test_default_socket_path_unix() {
56        let path = default_ipc_socket_path();
57        assert!(path.to_string_lossy().ends_with("sentinelpass.sock"));
58    }
59
60    #[cfg(windows)]
61    #[test]
62    fn test_default_socket_path_windows() {
63        let path = default_ipc_socket_path();
64        assert!(path.to_string_lossy().contains("\\\\.\\pipe\\"));
65    }
66
67    /// Env vars are process-global: these two tests mutate
68    /// XDG_RUNTIME_DIR and run in parallel by default, so they serialize
69    /// on this lock (a race here fails the not-tmp assertion flakily).
70    static XDG_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
71
72    /// WBS-507: the default socket path uses $XDG_RUNTIME_DIR when present,
73    /// nested in the private SentinelPass runtime dir — never bare /tmp.
74    #[test]
75    fn test_socket_path_with_xdg_runtime_dir() {
76        let _guard = XDG_ENV_LOCK.lock().unwrap();
77        let custom_runtime = "/tmp/custom_runtime";
78        std::env::set_var("XDG_RUNTIME_DIR", custom_runtime);
79
80        let path = default_ipc_socket_path();
81
82        #[cfg(unix)]
83        {
84            let path_str = path.to_string_lossy();
85            assert!(
86                path_str.contains(custom_runtime) && path_str.contains("SentinelPass"),
87                "default socket must live in the private runtime dir: {path_str}"
88            );
89        }
90
91        #[cfg(windows)]
92        {
93            // On Windows, just verify the function runs without error
94            let _ = path;
95        }
96
97        std::env::remove_var("XDG_RUNTIME_DIR");
98    }
99
100    /// WBS-507: the /tmp fallback is REMOVED — with XDG_RUNTIME_DIR unset,
101    /// the default falls back to the config dir's private runtime subdir.
102    #[test]
103    fn test_socket_path_without_xdg_runtime_dir_is_not_tmp() {
104        let _guard = XDG_ENV_LOCK.lock().unwrap();
105        std::env::remove_var("XDG_RUNTIME_DIR");
106        let path = default_ipc_socket_path();
107        let path_str = path.to_string_lossy().to_string();
108
109        #[cfg(unix)]
110        assert!(
111            !path_str.starts_with("/tmp/") && !path_str.starts_with("/private/tmp/"),
112            "the /tmp fallback must stay removed: {path_str}"
113        );
114        assert!(
115            path_str.contains("runtime") || path_str.contains("SentinelPass"),
116            "default must use the private runtime dir: {path_str}"
117        );
118    }
119}