1use std::path::PathBuf;
2
3pub 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
13pub 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
46pub fn koi_state_dir() -> PathBuf {
48 koi_data_dir().join("state")
49}
50
51pub fn koi_log_dir() -> PathBuf {
53 koi_data_dir().join("logs")
54}
55
56pub 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 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 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 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}