open_sesame/util/
paths.rs

1//! Secure path management for Open Sesame
2//!
3//! Provides centralized path management with proper permission enforcement.
4//! All runtime data goes into ~/.cache/open-sesame/ with 700 permissions.
5//! Configuration data uses ~/.config/open-sesame/ via dirs::config_dir().
6
7use crate::util::{Error, Result};
8use std::fs;
9use std::os::unix::fs::PermissionsExt;
10use std::path::PathBuf;
11
12/// Secure directory permissions (owner read/write/execute only)
13const SECURE_DIR_MODE: u32 = 0o700;
14
15/// Returns the open-sesame cache directory, creating with secure permissions if needed.
16///
17/// Returns ~/.cache/open-sesame/ with 700 permissions.
18/// Fails when HOME is not set or directory cannot be created with proper permissions.
19///
20/// # Security
21/// - Never falls back to /tmp or other world-accessible locations
22/// - Enforces 700 permissions on the directory
23/// - Validates permissions on existing directories
24pub fn cache_dir() -> Result<PathBuf> {
25    let base = dirs::cache_dir()
26        .or_else(|| {
27            // Fallback to ~/.cache when XDG_CACHE_HOME not set
28            dirs::home_dir().map(|h| h.join(".cache"))
29        })
30        .ok_or_else(|| {
31            Error::Other(
32                "Cannot determine cache directory: HOME environment variable not set".to_string(),
33            )
34        })?;
35
36    let cache_path = base.join("open-sesame");
37    ensure_secure_dir(&cache_path)?;
38    Ok(cache_path)
39}
40
41/// Returns the open-sesame config directory.
42///
43/// Returns ~/.config/open-sesame/.
44/// For COSMIC shortcuts, cosmic_config_dir() provides the appropriate path.
45pub fn config_dir() -> Result<PathBuf> {
46    let base = dirs::config_dir()
47        .or_else(|| dirs::home_dir().map(|h| h.join(".config")))
48        .ok_or_else(|| {
49            Error::Other(
50                "Cannot determine config directory: HOME environment variable not set".to_string(),
51            )
52        })?;
53
54    Ok(base.join("open-sesame"))
55}
56
57/// Returns the COSMIC shortcuts configuration directory.
58///
59/// Provides path to COSMIC's custom shortcuts config file.
60pub fn cosmic_shortcuts_path() -> Result<PathBuf> {
61    let base = dirs::config_dir()
62        .or_else(|| dirs::home_dir().map(|h| h.join(".config")))
63        .ok_or_else(|| {
64            Error::Other(
65                "Cannot determine config directory: HOME environment variable not set".to_string(),
66            )
67        })?;
68
69    Ok(base.join("cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom"))
70}
71
72/// Returns the lock file path.
73///
74/// Path: ~/.cache/open-sesame/instance.lock
75pub fn lock_file() -> Result<PathBuf> {
76    Ok(cache_dir()?.join("instance.lock"))
77}
78
79/// Returns the MRU state file path.
80///
81/// Path: ~/.cache/open-sesame/mru
82pub fn mru_file() -> Result<PathBuf> {
83    Ok(cache_dir()?.join("mru"))
84}
85
86/// Returns the log file path.
87///
88/// Path: ~/.cache/open-sesame/debug.log
89pub fn log_file() -> Result<PathBuf> {
90    Ok(cache_dir()?.join("debug.log"))
91}
92
93/// Ensures a directory exists with secure permissions (700).
94///
95/// Creates directory when nonexistent.
96/// Validates and fixes permissions when directory exists.
97fn ensure_secure_dir(path: &PathBuf) -> Result<()> {
98    if path.exists() {
99        // Directory verification
100        if !path.is_dir() {
101            return Err(Error::Other(format!(
102                "{} exists but is not a directory",
103                path.display()
104            )));
105        }
106
107        // Permission validation and correction
108        let metadata = fs::metadata(path).map_err(|e| {
109            Error::Other(format!(
110                "Failed to read metadata for {}: {}",
111                path.display(),
112                e
113            ))
114        })?;
115
116        let current_mode = metadata.permissions().mode() & 0o777;
117        if current_mode != SECURE_DIR_MODE {
118            tracing::warn!(
119                "Fixing permissions on {} from {:o} to {:o}",
120                path.display(),
121                current_mode,
122                SECURE_DIR_MODE
123            );
124            fs::set_permissions(path, fs::Permissions::from_mode(SECURE_DIR_MODE)).map_err(
125                |e| {
126                    Error::Other(format!(
127                        "Failed to set permissions on {}: {}",
128                        path.display(),
129                        e
130                    ))
131                },
132            )?;
133        }
134    } else {
135        // Directory creation with secure permissions
136        fs::create_dir_all(path).map_err(|e| {
137            Error::Other(format!(
138                "Failed to create directory {}: {}",
139                path.display(),
140                e
141            ))
142        })?;
143
144        fs::set_permissions(path, fs::Permissions::from_mode(SECURE_DIR_MODE)).map_err(|e| {
145            Error::Other(format!(
146                "Failed to set permissions on {}: {}",
147                path.display(),
148                e
149            ))
150        })?;
151
152        tracing::debug!(
153            "Created secure directory: {} (mode {:o})",
154            path.display(),
155            SECURE_DIR_MODE
156        );
157    }
158
159    Ok(())
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_cache_dir_structure() {
168        // Test requires HOME environment variable
169        if std::env::var("HOME").is_err() {
170            return;
171        }
172
173        let cache = cache_dir().expect("Should get cache dir");
174        assert!(cache.ends_with("open-sesame"));
175        assert!(cache.to_string_lossy().contains(".cache"));
176    }
177
178    #[test]
179    fn test_lock_file_path() {
180        if std::env::var("HOME").is_err() {
181            return;
182        }
183
184        let lock = lock_file().expect("Should get lock file path");
185        assert!(lock.ends_with("instance.lock"));
186    }
187
188    #[test]
189    fn test_mru_file_path() {
190        if std::env::var("HOME").is_err() {
191            return;
192        }
193
194        let mru = mru_file().expect("Should get MRU file path");
195        assert!(mru.ends_with("mru"));
196    }
197}