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 resolve_audit_store() -> AuditStoreChoice {
127 let explicit = std::env::var_os("SYSKNIFE_DATABASE_PATH").map(PathBuf::from);
128 let per_user = default_database_path();
129 let system = PathBuf::from(PRODUCTION_DATABASE_PATH);
130 let per_user_exists = per_user.exists();
131 let system_exists = system.exists();
132 choose_audit_store(explicit, per_user, per_user_exists, system_exists)
133}
134
135fn current_uid() -> u32 {
142 std::fs::read_to_string("/proc/self/status")
143 .ok()
144 .and_then(|s| {
145 s.lines()
146 .find(|l| l.starts_with("Uid:"))
147 .and_then(|l| l.split_whitespace().nth(1))
148 .and_then(|v| v.parse::<u32>().ok())
149 })
150 .unwrap_or(0)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::{default_database_path, default_listen_uri, PRODUCTION_DATABASE_PATH};
156 use std::sync::Mutex;
157
158 static ENV_LOCK: Mutex<()> = Mutex::new(());
159
160 #[test]
161 fn production_database_path_is_absolute() {
162 assert!(PRODUCTION_DATABASE_PATH.starts_with("/var/lib/"));
163 }
164
165 #[test]
166 fn database_env_var_wins() {
167 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
168 unsafe {
169 std::env::set_var("SYSKNIFE_DATABASE_PATH", "/explicit/path/db.sqlite");
170 }
171 let p = default_database_path();
172 unsafe {
173 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
174 }
175 assert_eq!(p.to_str(), Some("/explicit/path/db.sqlite"));
176 }
177
178 #[test]
179 fn database_xdg_state_used_when_set() {
180 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
181 unsafe {
182 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
183 std::env::set_var("XDG_STATE_HOME", "/xdg/state");
184 }
185 let p = default_database_path();
186 unsafe {
187 std::env::remove_var("XDG_STATE_HOME");
188 }
189 assert_eq!(p.to_str(), Some("/xdg/state/sysknife/daemon.sqlite"));
190 }
191
192 #[test]
193 fn database_falls_back_to_home_local_state() {
194 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
195 unsafe {
196 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
197 std::env::remove_var("XDG_STATE_HOME");
198 std::env::set_var("HOME", "/home/contributor");
199 }
200 let p = default_database_path();
201 unsafe {
202 std::env::remove_var("HOME");
203 }
204 assert_eq!(
205 p.to_str(),
206 Some("/home/contributor/.local/state/sysknife/daemon.sqlite")
207 );
208 }
209
210 #[test]
211 fn database_last_resort_is_production_path() {
212 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
213 let saved_home = std::env::var("HOME").ok();
215 unsafe {
216 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
217 std::env::remove_var("XDG_STATE_HOME");
218 std::env::remove_var("HOME");
219 }
220 let p = default_database_path();
221 unsafe {
222 if let Some(h) = saved_home {
223 std::env::set_var("HOME", h);
224 }
225 }
226 assert_eq!(p.to_str(), Some(PRODUCTION_DATABASE_PATH));
227 }
228
229 #[test]
230 fn listen_env_var_wins() {
231 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
232 unsafe {
233 std::env::set_var("SYSKNIFE_LISTEN_URI", "unix:///explicit.sock");
234 }
235 let u = default_listen_uri();
236 unsafe {
237 std::env::remove_var("SYSKNIFE_LISTEN_URI");
238 }
239 assert_eq!(u, "unix:///explicit.sock");
240 }
241
242 #[test]
243 fn listen_uses_xdg_runtime_dir_when_set() {
244 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
245 unsafe {
246 std::env::remove_var("SYSKNIFE_LISTEN_URI");
247 std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
248 }
249 let u = default_listen_uri();
250 unsafe {
251 std::env::remove_var("XDG_RUNTIME_DIR");
252 }
253 assert_eq!(u, "unix:///run/user/1000/sysknife/daemon.sock");
254 }
255
256 #[test]
257 fn listen_last_resort_is_per_uid_tmp() {
258 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
259 unsafe {
260 std::env::remove_var("SYSKNIFE_LISTEN_URI");
261 std::env::remove_var("XDG_RUNTIME_DIR");
262 }
263 let u = default_listen_uri();
264 assert!(u.starts_with("unix:///tmp/sysknife-"));
265 assert!(u.ends_with(".sock"));
266 }
267
268 use std::path::{Path, PathBuf};
273
274 #[test]
275 fn an_explicit_database_path_is_never_second_guessed() {
276 let choice = super::choose_audit_store(
277 Some(PathBuf::from("/srv/audit.sqlite")),
278 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
279 true,
280 true,
281 );
282 assert_eq!(choice.path(), Path::new("/srv/audit.sqlite"));
283 assert_eq!(
284 choice.note(),
285 None,
286 "an explicit choice needs no explanation"
287 );
288 }
289
290 #[test]
291 fn a_present_per_user_store_wins_over_the_system_one() {
292 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
293 let choice = super::choose_audit_store(None, per_user.clone(), true, true);
294 assert_eq!(choice.path(), per_user.as_path());
295 }
296
297 #[test]
300 fn the_system_store_is_used_when_no_per_user_store_exists() {
301 let choice = super::choose_audit_store(
302 None,
303 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
304 false,
305 true,
306 );
307 assert_eq!(choice.path(), Path::new(super::PRODUCTION_DATABASE_PATH));
308 let note = choice
309 .note()
310 .expect("reading another store must be announced");
311 assert!(
312 note.contains(super::PRODUCTION_DATABASE_PATH),
313 "note names the store: {note}"
314 );
315 }
316
317 #[test]
318 fn with_no_store_anywhere_the_per_user_path_is_reported() {
319 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
320 let choice = super::choose_audit_store(None, per_user.clone(), false, false);
321 assert_eq!(choice.path(), per_user.as_path());
322 assert_eq!(choice.note(), None);
323 }
324
325 #[test]
329 fn the_system_store_constant_matches_the_packaged_unit() {
330 let unit = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
331 .join("../../packaging/sysknife-daemon.service");
332 let contents = std::fs::read_to_string(&unit)
333 .unwrap_or_else(|e| panic!("read {}: {e}", unit.display()));
334 let expected = format!("SYSKNIFE_DATABASE_PATH={}", super::PRODUCTION_DATABASE_PATH);
335 assert!(
336 contents.contains(&expected),
337 "packaged unit must set {expected}; got:\n{contents}"
338 );
339 }
340}