1use std::path::{Path, PathBuf};
4
5pub mod action_family;
6pub mod config;
7pub mod distro;
8
9pub const PRODUCTION_DATABASE_PATH: &str = "/var/lib/sysknife/daemon.sqlite";
13
14pub fn default_listen_uri() -> String {
24 if let Ok(uri) = std::env::var("SYSKNIFE_LISTEN_URI") {
25 return uri;
26 }
27 if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
28 let p = PathBuf::from(runtime).join("sysknife/daemon.sock");
29 return format!("unix://{}", p.display());
30 }
31 let uid = current_uid();
32 format!("unix:///tmp/sysknife-{uid}.sock")
33}
34
35pub fn default_database_path() -> PathBuf {
44 if let Ok(path) = std::env::var("SYSKNIFE_DATABASE_PATH") {
45 return PathBuf::from(path);
46 }
47 if let Ok(state) = std::env::var("XDG_STATE_HOME") {
48 return PathBuf::from(state).join("sysknife/daemon.sqlite");
49 }
50 if let Ok(home) = std::env::var("HOME") {
51 return PathBuf::from(home).join(".local/state/sysknife/daemon.sqlite");
52 }
53 PathBuf::from(PRODUCTION_DATABASE_PATH)
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum AuditStoreChoice {
71 Explicit(PathBuf),
74 PerUser(PathBuf),
76 System(PathBuf),
78}
79
80impl AuditStoreChoice {
81 pub fn path(&self) -> &Path {
83 match self {
84 Self::Explicit(p) | Self::PerUser(p) | Self::System(p) => p,
85 }
86 }
87
88 pub fn note(&self) -> Option<String> {
92 match self {
93 Self::System(path) => Some(format!(
94 "no per-user audit store found; reading the system daemon's store at {}",
95 path.display()
96 )),
97 Self::Explicit(_) | Self::PerUser(_) => None,
98 }
99 }
100}
101
102pub fn choose_audit_store(
105 explicit: Option<PathBuf>,
106 per_user: PathBuf,
107 per_user_exists: bool,
108 system_exists: bool,
109) -> AuditStoreChoice {
110 if let Some(path) = explicit {
111 return AuditStoreChoice::Explicit(path);
112 }
113 if per_user_exists {
114 return AuditStoreChoice::PerUser(per_user);
115 }
116 if system_exists {
117 return AuditStoreChoice::System(PathBuf::from(PRODUCTION_DATABASE_PATH));
118 }
119 AuditStoreChoice::PerUser(per_user)
122}
123
124pub fn path_is_present(path: &Path) -> bool {
132 path_is_present_result(std::fs::metadata(path).map(|_| ()))
133}
134
135pub(crate) fn path_is_present_result(result: Result<(), std::io::Error>) -> bool {
140 match result {
141 Ok(()) => true,
142 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => true,
143 Err(_) => false,
144 }
145}
146
147pub fn resolve_audit_store() -> AuditStoreChoice {
150 let explicit = std::env::var_os("SYSKNIFE_DATABASE_PATH").map(PathBuf::from);
151 let per_user = default_database_path();
152 let system = PathBuf::from(PRODUCTION_DATABASE_PATH);
153 let per_user_exists = path_is_present(&per_user);
154 let system_exists = path_is_present(&system);
155 choose_audit_store(explicit, per_user, per_user_exists, system_exists)
156}
157
158fn current_uid() -> u32 {
165 std::fs::read_to_string("/proc/self/status")
166 .ok()
167 .and_then(|s| {
168 s.lines()
169 .find(|l| l.starts_with("Uid:"))
170 .and_then(|l| l.split_whitespace().nth(1))
171 .and_then(|v| v.parse::<u32>().ok())
172 })
173 .unwrap_or(0)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::{default_database_path, default_listen_uri, PRODUCTION_DATABASE_PATH};
179 use std::sync::Mutex;
180
181 static ENV_LOCK: Mutex<()> = Mutex::new(());
182
183 #[test]
184 fn production_database_path_is_absolute() {
185 assert!(PRODUCTION_DATABASE_PATH.starts_with("/var/lib/"));
186 }
187
188 #[test]
189 fn database_env_var_wins() {
190 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
191 unsafe {
192 std::env::set_var("SYSKNIFE_DATABASE_PATH", "/explicit/path/db.sqlite");
193 }
194 let p = default_database_path();
195 unsafe {
196 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
197 }
198 assert_eq!(p.to_str(), Some("/explicit/path/db.sqlite"));
199 }
200
201 #[test]
202 fn database_xdg_state_used_when_set() {
203 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
204 unsafe {
205 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
206 std::env::set_var("XDG_STATE_HOME", "/xdg/state");
207 }
208 let p = default_database_path();
209 unsafe {
210 std::env::remove_var("XDG_STATE_HOME");
211 }
212 assert_eq!(p.to_str(), Some("/xdg/state/sysknife/daemon.sqlite"));
213 }
214
215 #[test]
216 fn database_falls_back_to_home_local_state() {
217 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
218 unsafe {
219 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
220 std::env::remove_var("XDG_STATE_HOME");
221 std::env::set_var("HOME", "/home/contributor");
222 }
223 let p = default_database_path();
224 unsafe {
225 std::env::remove_var("HOME");
226 }
227 assert_eq!(
228 p.to_str(),
229 Some("/home/contributor/.local/state/sysknife/daemon.sqlite")
230 );
231 }
232
233 #[test]
234 fn database_last_resort_is_production_path() {
235 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
236 let saved_home = std::env::var("HOME").ok();
238 unsafe {
239 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
240 std::env::remove_var("XDG_STATE_HOME");
241 std::env::remove_var("HOME");
242 }
243 let p = default_database_path();
244 unsafe {
245 if let Some(h) = saved_home {
246 std::env::set_var("HOME", h);
247 }
248 }
249 assert_eq!(p.to_str(), Some(PRODUCTION_DATABASE_PATH));
250 }
251
252 #[test]
253 fn listen_env_var_wins() {
254 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
255 unsafe {
256 std::env::set_var("SYSKNIFE_LISTEN_URI", "unix:///explicit.sock");
257 }
258 let u = default_listen_uri();
259 unsafe {
260 std::env::remove_var("SYSKNIFE_LISTEN_URI");
261 }
262 assert_eq!(u, "unix:///explicit.sock");
263 }
264
265 #[test]
266 fn listen_uses_xdg_runtime_dir_when_set() {
267 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
268 unsafe {
269 std::env::remove_var("SYSKNIFE_LISTEN_URI");
270 std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
271 }
272 let u = default_listen_uri();
273 unsafe {
274 std::env::remove_var("XDG_RUNTIME_DIR");
275 }
276 assert_eq!(u, "unix:///run/user/1000/sysknife/daemon.sock");
277 }
278
279 #[test]
280 fn listen_last_resort_is_per_uid_tmp() {
281 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
282 unsafe {
283 std::env::remove_var("SYSKNIFE_LISTEN_URI");
284 std::env::remove_var("XDG_RUNTIME_DIR");
285 }
286 let u = default_listen_uri();
287 assert!(u.starts_with("unix:///tmp/sysknife-"));
288 assert!(u.ends_with(".sock"));
289 }
290
291 use std::path::{Path, PathBuf};
296
297 #[test]
298 fn an_explicit_database_path_is_never_second_guessed() {
299 let choice = super::choose_audit_store(
300 Some(PathBuf::from("/srv/audit.sqlite")),
301 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
302 true,
303 true,
304 );
305 assert_eq!(choice.path(), Path::new("/srv/audit.sqlite"));
306 assert_eq!(
307 choice.note(),
308 None,
309 "an explicit choice needs no explanation"
310 );
311 }
312
313 #[test]
314 fn a_present_per_user_store_wins_over_the_system_one() {
315 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
316 let choice = super::choose_audit_store(None, per_user.clone(), true, true);
317 assert_eq!(choice.path(), per_user.as_path());
318 }
319
320 #[test]
323 fn the_system_store_is_used_when_no_per_user_store_exists() {
324 let choice = super::choose_audit_store(
325 None,
326 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
327 false,
328 true,
329 );
330 assert_eq!(choice.path(), Path::new(super::PRODUCTION_DATABASE_PATH));
331 let note = choice
332 .note()
333 .expect("reading another store must be announced");
334 assert!(
335 note.contains(super::PRODUCTION_DATABASE_PATH),
336 "note names the store: {note}"
337 );
338 }
339
340 #[test]
341 fn with_no_store_anywhere_the_per_user_path_is_reported() {
342 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
343 let choice = super::choose_audit_store(None, per_user.clone(), false, false);
344 assert_eq!(choice.path(), per_user.as_path());
345 assert_eq!(choice.note(), None);
346 }
347
348 #[test]
349 fn permission_denied_counts_as_a_present_store() {
350 assert!(
351 super::path_is_present_result(Ok(())),
352 "a successful stat is present"
353 );
354 assert!(
355 super::path_is_present_result(Err(std::io::Error::from(
356 std::io::ErrorKind::PermissionDenied
357 ))),
358 "EACCES must not be treated as missing"
359 );
360 assert!(
361 !super::path_is_present_result(Err(std::io::Error::from(std::io::ErrorKind::NotFound))),
362 "ENOENT remains missing"
363 );
364 }
365
366 #[test]
370 fn unreadable_system_store_wins_over_a_missing_per_user_store() {
371 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
372 let system_exists = super::path_is_present_result(Err(std::io::Error::from(
373 std::io::ErrorKind::PermissionDenied,
374 )));
375 let choice = super::choose_audit_store(None, per_user, false, system_exists);
376 assert_eq!(
377 choice.path(),
378 Path::new(super::PRODUCTION_DATABASE_PATH),
379 "EACCES on the system store must not fall through to PerUser"
380 );
381 assert!(
382 choice.note().is_some(),
383 "System choice must still announce itself"
384 );
385 }
386
387 #[cfg(unix)]
390 #[test]
391 fn path_behind_mode_000_directory_counts_as_present() {
392 use std::os::unix::fs::PermissionsExt;
393
394 let root = tempfile::tempdir().expect("tempdir");
395 let locked = root.path().join("locked");
396 std::fs::create_dir(&locked).expect("mkdir");
397 let db = locked.join("daemon.sqlite");
398 std::fs::write(&db, b"x").expect("write db");
399 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000))
400 .expect("chmod 000");
401
402 let present = super::path_is_present(&db);
403 let exists_under_eacces = db.exists();
404
405 let _ = std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o700));
407 assert!(
408 present,
409 "metadata EACCES on a 0700/000 store must count as present, not missing"
410 );
411 assert!(
412 !exists_under_eacces,
413 "precondition: Path::exists is false under EACCES (the bug we are fixing)"
414 );
415 }
416
417 #[test]
421 fn the_system_store_constant_matches_the_packaged_unit() {
422 let unit = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
423 .join("../../packaging/sysknife-daemon.service");
424 let contents = std::fs::read_to_string(&unit)
425 .unwrap_or_else(|e| panic!("read {}: {e}", unit.display()));
426 let expected = format!("SYSKNIFE_DATABASE_PATH={}", super::PRODUCTION_DATABASE_PATH);
427 assert!(
428 contents.contains(&expected),
429 "packaged unit must set {expected}; got:\n{contents}"
430 );
431 }
432}