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 paused_until_path() -> PathBuf {
204 state_dir().join("paused_until")
205}
206
207pub fn read_probe_id() -> Option<u32> {
211 let path = probe_id_path();
212 match std::fs::read_to_string(&path) {
213 Ok(content) => content.trim().parse().ok(),
214 Err(_) => None,
215 }
216}
217
218pub fn write_probe_id(probe_id: u32) -> std::io::Result<()> {
222 let dir = state_dir();
223 ensure_dir(&dir)?;
224 let path = probe_id_path();
225 std::fs::write(&path, probe_id.to_string())
226}
227
228#[cfg(unix)]
230fn is_root() -> bool {
231 unsafe { libc::getuid() == 0 }
233}
234
235#[cfg(not(unix))]
236fn is_root() -> bool {
237 false
238}
239
240fn home_dir() -> Option<PathBuf> {
242 if let Ok(home) = std::env::var("HOME") {
243 return Some(PathBuf::from(home));
244 }
245
246 #[cfg(windows)]
247 if let Ok(home) = std::env::var("USERPROFILE") {
248 return Some(PathBuf::from(home));
249 }
250
251 None
252}
253
254pub fn ensure_dir(path: &PathBuf) -> std::io::Result<()> {
256 if !path.exists() {
257 std::fs::create_dir_all(path)?;
258 }
259 Ok(())
260}
261
262pub fn ensure_config_dir() -> std::io::Result<PathBuf> {
264 let dir = config_dir();
265 ensure_dir(&dir)?;
266 Ok(dir)
267}
268
269pub fn ensure_state_dir() -> std::io::Result<PathBuf> {
271 let dir = state_dir();
272 ensure_dir(&dir)?;
273 Ok(dir)
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use std::sync::Mutex;
280
281 static ENV_LOCK: Mutex<()> = Mutex::new(());
283
284 #[test]
285 fn test_config_dir_with_env() {
286 let _guard = ENV_LOCK.lock().unwrap();
287
288 let original = std::env::var("CONFIGURATION_DIRECTORY").ok();
289
290 unsafe { std::env::set_var("CONFIGURATION_DIRECTORY", "/test/config") };
291 assert_eq!(config_dir(), PathBuf::from("/test/config"));
292
293 if let Some(val) = original {
294 unsafe { std::env::set_var("CONFIGURATION_DIRECTORY", val) };
295 } else {
296 unsafe { std::env::remove_var("CONFIGURATION_DIRECTORY") };
297 }
298 }
299
300 #[test]
301 fn test_state_dir_with_env() {
302 let _guard = ENV_LOCK.lock().unwrap();
303
304 let original = std::env::var("STATE_DIRECTORY").ok();
305
306 unsafe { std::env::set_var("STATE_DIRECTORY", "/test/state") };
307 assert_eq!(state_dir(), PathBuf::from("/test/state"));
308
309 if let Some(val) = original {
310 unsafe { std::env::set_var("STATE_DIRECTORY", val) };
311 } else {
312 unsafe { std::env::remove_var("STATE_DIRECTORY") };
313 }
314 }
315
316 #[test]
317 fn test_xdg_config_fallback() {
318 let _guard = ENV_LOCK.lock().unwrap();
319
320 let orig_conf_dir = std::env::var("CONFIGURATION_DIRECTORY").ok();
321 let orig_xdg = std::env::var("XDG_CONFIG_HOME").ok();
322
323 unsafe { std::env::remove_var("CONFIGURATION_DIRECTORY") };
324 unsafe { std::env::set_var("XDG_CONFIG_HOME", "/home/test/.config") };
325
326 assert_eq!(config_dir(), PathBuf::from("/home/test/.config/starla"));
327
328 if let Some(val) = orig_conf_dir {
329 unsafe { std::env::set_var("CONFIGURATION_DIRECTORY", val) };
330 }
331 if let Some(val) = orig_xdg {
332 unsafe { std::env::set_var("XDG_CONFIG_HOME", val) };
333 } else {
334 unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
335 }
336 }
337
338 #[test]
339 fn test_xdg_state_fallback() {
340 let _guard = ENV_LOCK.lock().unwrap();
341
342 let orig_state_dir = std::env::var("STATE_DIRECTORY").ok();
343 let orig_xdg = std::env::var("XDG_STATE_HOME").ok();
344
345 unsafe { std::env::remove_var("STATE_DIRECTORY") };
346 unsafe { std::env::set_var("XDG_STATE_HOME", "/home/test/.local/state") };
347
348 assert_eq!(state_dir(), PathBuf::from("/home/test/.local/state/starla"));
349
350 if let Some(val) = orig_state_dir {
351 unsafe { std::env::set_var("STATE_DIRECTORY", val) };
352 }
353 if let Some(val) = orig_xdg {
354 unsafe { std::env::set_var("XDG_STATE_HOME", val) };
355 } else {
356 unsafe { std::env::remove_var("XDG_STATE_HOME") };
357 }
358 }
359
360 #[test]
361 fn test_default_file_paths() {
362 let config = config_file();
363 assert!(config.to_string_lossy().contains("config.toml"));
364
365 let key = probe_key_path();
366 assert!(key.to_string_lossy().contains("probe_key"));
367
368 let pid = probe_id_path();
369 assert!(pid.to_string_lossy().contains("probe_id"));
370
371 let kh = known_hosts_path();
372 assert!(kh.to_string_lossy().contains("known_hosts"));
373 }
374
375 #[test]
376 fn test_probe_id_read_write() {
377 let temp_dir =
378 std::env::temp_dir().join(format!("starla-test-probe-id-{}", std::process::id()));
379 let _ = std::fs::remove_dir_all(&temp_dir);
380 std::fs::create_dir_all(&temp_dir).unwrap();
381
382 let probe_id_file = temp_dir.join("probe_id");
383 assert!(!probe_id_file.exists());
384
385 std::fs::write(&probe_id_file, "1014036").unwrap();
386
387 let content = std::fs::read_to_string(&probe_id_file).unwrap();
388 let parsed: Option<u32> = content.trim().parse().ok();
389 assert_eq!(parsed, Some(1014036));
390
391 let _ = std::fs::remove_dir_all(&temp_dir);
392 }
393}