Skip to main content

koi_common/
paths.rs

1use std::path::PathBuf;
2
3/// Resolve the data directory with an explicit override, falling back to
4/// the env-var / platform default.  Use this in contexts where
5/// `std::env::set_var` would be unsound (e.g. multi-threaded embedded).
6pub fn koi_data_dir_with_override(override_dir: Option<&std::path::Path>) -> PathBuf {
7    if let Some(dir) = override_dir {
8        return dir.to_path_buf();
9    }
10    koi_data_dir()
11}
12
13/// Root data directory for Koi.
14///
15/// All Koi data is machine-scoped (CA keys, roster, certs, logs, state).
16/// The user never owns the data - certificates belong to the machine.
17///
18/// - Linux: `/var/lib/koi/`
19/// - macOS: `/Library/Application Support/koi/`
20/// - Windows: `%ProgramData%\koi\`
21///
22/// Override with `KOI_DATA_DIR` env var (for testing).
23pub fn koi_data_dir() -> PathBuf {
24    if let Ok(override_dir) = std::env::var("KOI_DATA_DIR") {
25        return PathBuf::from(override_dir);
26    }
27
28    #[cfg(target_os = "macos")]
29    {
30        PathBuf::from("/Library/Application Support/koi")
31    }
32
33    #[cfg(windows)]
34    {
35        let program_data =
36            std::env::var("ProgramData").unwrap_or_else(|_| r"C:\ProgramData".to_string());
37        PathBuf::from(program_data).join("koi")
38    }
39
40    #[cfg(not(any(target_os = "macos", windows)))]
41    {
42        PathBuf::from("/var/lib/koi")
43    }
44}
45
46/// Runtime state directory.
47pub fn koi_state_dir() -> PathBuf {
48    koi_data_dir().join("state")
49}
50
51/// Log directory.
52pub fn koi_log_dir() -> PathBuf {
53    koi_data_dir().join("logs")
54}
55
56/// Certificate directory (used by certmesh).
57pub fn koi_certs_dir() -> PathBuf {
58    koi_data_dir().join("certs")
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use std::sync::Mutex;
65
66    /// Tests that call `koi_data_dir()` must hold this lock because
67    /// `koi_data_dir_env_override` mutates the `KOI_DATA_DIR` env var
68    /// and parallel tests would see inconsistent values.
69    static ENV_LOCK: Mutex<()> = Mutex::new(());
70
71    #[test]
72    fn data_dir_ends_with_koi() {
73        let _lock = ENV_LOCK.lock().unwrap();
74        let dir = koi_data_dir();
75        assert!(
76            dir.ends_with("koi"),
77            "data dir should end with 'koi': {dir:?}"
78        );
79    }
80
81    #[test]
82    fn data_dir_is_not_empty() {
83        let _lock = ENV_LOCK.lock().unwrap();
84        let dir = koi_data_dir();
85        assert!(dir.components().count() > 0);
86    }
87
88    #[test]
89    fn state_dir_is_child_of_data_dir() {
90        let _lock = ENV_LOCK.lock().unwrap();
91        let data = koi_data_dir();
92        let state = koi_state_dir();
93        assert!(state.starts_with(&data));
94        assert!(state.ends_with("state"));
95    }
96
97    #[test]
98    fn log_dir_is_child_of_data_dir() {
99        let _lock = ENV_LOCK.lock().unwrap();
100        let data = koi_data_dir();
101        let logs = koi_log_dir();
102        assert!(logs.starts_with(&data));
103        assert!(logs.ends_with("logs"));
104    }
105
106    #[test]
107    fn certs_dir_is_child_of_data_dir() {
108        let _lock = ENV_LOCK.lock().unwrap();
109        let data = koi_data_dir();
110        let certs = koi_certs_dir();
111        assert!(certs.starts_with(&data));
112        assert!(certs.ends_with("certs"));
113    }
114
115    #[test]
116    fn subdirs_are_distinct() {
117        let _lock = ENV_LOCK.lock().unwrap();
118        let state = koi_state_dir();
119        let logs = koi_log_dir();
120        let certs = koi_certs_dir();
121        assert_ne!(state, logs);
122        assert_ne!(state, certs);
123        assert_ne!(logs, certs);
124    }
125
126    #[cfg(windows)]
127    #[test]
128    fn windows_uses_programdata() {
129        let _lock = ENV_LOCK.lock().unwrap();
130        let dir = koi_data_dir();
131        let dir_str = dir.to_string_lossy().to_lowercase();
132        assert!(
133            dir_str.contains("programdata"),
134            "Windows data dir should use ProgramData: {dir:?}"
135        );
136    }
137
138    #[cfg(target_os = "macos")]
139    #[test]
140    fn macos_uses_system_library() {
141        let _lock = ENV_LOCK.lock().unwrap();
142        let dir = koi_data_dir();
143        let dir_str = dir.to_string_lossy();
144        assert!(
145            dir_str.starts_with("/Library/Application Support"),
146            "macOS data dir should be in /Library/Application Support: {dir:?}"
147        );
148    }
149
150    #[cfg(not(any(target_os = "macos", windows)))]
151    #[test]
152    fn linux_uses_var_lib() {
153        let _lock = ENV_LOCK.lock().unwrap();
154        let dir = koi_data_dir();
155        let dir_str = dir.to_string_lossy();
156        assert!(
157            dir_str.starts_with("/var/lib/koi"),
158            "Linux data dir should be /var/lib/koi: {dir:?}"
159        );
160    }
161
162    #[test]
163    fn koi_data_dir_env_override() {
164        let _lock = ENV_LOCK.lock().unwrap();
165
166        // Save and set override
167        let prev = std::env::var("KOI_DATA_DIR").ok();
168        std::env::set_var("KOI_DATA_DIR", "/tmp/koi-test-override");
169        let dir = koi_data_dir();
170        assert_eq!(dir, PathBuf::from("/tmp/koi-test-override"));
171
172        // Restore
173        match prev {
174            Some(v) => std::env::set_var("KOI_DATA_DIR", v),
175            None => std::env::remove_var("KOI_DATA_DIR"),
176        }
177    }
178}