1use std::path::PathBuf;
25use std::sync::OnceLock;
26
27const APP_NAME: &str = "starla";
29
30fn 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
42static STATE_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
44
45static RUNTIME_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
48
49pub fn set_state_dir(path: PathBuf) {
52 let _ = STATE_DIR_OVERRIDE.set(path);
53}
54
55pub fn set_runtime_dir(path: PathBuf) {
59 let _ = RUNTIME_DIR_OVERRIDE.set(path);
60}
61
62pub 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
93pub 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
129pub 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 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
172pub fn config_file() -> PathBuf {
174 config_dir().join("config.toml")
175}
176
177pub fn probe_key_path() -> PathBuf {
179 state_dir().join("probe_key")
180}
181
182pub fn probe_pubkey_path() -> PathBuf {
184 state_dir().join("probe_key.pub")
185}
186
187pub fn known_hosts_path() -> PathBuf {
189 state_dir().join("known_hosts")
190}
191
192pub fn status_socket_path() -> PathBuf {
194 runtime_dir().join("starla.sock")
195}
196
197pub fn probe_id_path() -> PathBuf {
199 state_dir().join("probe_id")
200}
201
202pub 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
213pub 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#[cfg(unix)]
225fn is_root() -> bool {
226 unsafe { libc::getuid() == 0 }
228}
229
230#[cfg(not(unix))]
231fn is_root() -> bool {
232 false
233}
234
235fn 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
249pub fn ensure_dir(path: &PathBuf) -> std::io::Result<()> {
251 if !path.exists() {
252 std::fs::create_dir_all(path)?;
253 }
254 Ok(())
255}
256
257pub fn ensure_config_dir() -> std::io::Result<PathBuf> {
259 let dir = config_dir();
260 ensure_dir(&dir)?;
261 Ok(dir)
262}
263
264pub 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 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}