1use std::path::{Path, PathBuf};
4
5pub mod action_family;
6pub mod config;
7pub mod distro;
8
9pub use distro::{detect, detect_distro, parse_os_release, DistroFamily, DistroId};
10
11pub const PRODUCTION_LISTEN_URI: &str = "unix:///run/sysknife/daemon.sock";
15
16pub const PRODUCTION_DATABASE_PATH: &str = "/var/lib/sysknife/daemon.sqlite";
20
21pub fn default_listen_uri() -> String {
31 if let Ok(uri) = std::env::var("SYSKNIFE_LISTEN_URI") {
32 return uri;
33 }
34 if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
35 let p = PathBuf::from(runtime).join("sysknife/daemon.sock");
36 return format!("unix://{}", p.display());
37 }
38 let uid = current_uid();
39 format!("unix:///tmp/sysknife-{uid}.sock")
40}
41
42pub fn default_database_path() -> PathBuf {
51 if let Ok(path) = std::env::var("SYSKNIFE_DATABASE_PATH") {
52 return PathBuf::from(path);
53 }
54 if let Ok(state) = std::env::var("XDG_STATE_HOME") {
55 return PathBuf::from(state).join("sysknife/daemon.sqlite");
56 }
57 if let Ok(home) = std::env::var("HOME") {
58 return PathBuf::from(home).join(".local/state/sysknife/daemon.sqlite");
59 }
60 PathBuf::from(PRODUCTION_DATABASE_PATH)
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum AuditStoreChoice {
78 Explicit(PathBuf),
81 PerUser(PathBuf),
83 System(PathBuf),
85}
86
87impl AuditStoreChoice {
88 pub fn path(&self) -> &Path {
90 match self {
91 Self::Explicit(p) | Self::PerUser(p) | Self::System(p) => p,
92 }
93 }
94
95 pub fn note(&self) -> Option<String> {
99 match self {
100 Self::System(path) => Some(format!(
101 "no per-user audit store found; reading the system daemon's store at {}",
102 path.display()
103 )),
104 Self::Explicit(_) | Self::PerUser(_) => None,
105 }
106 }
107}
108
109pub fn choose_audit_store(
112 explicit: Option<PathBuf>,
113 per_user: PathBuf,
114 per_user_exists: bool,
115 system_exists: bool,
116) -> AuditStoreChoice {
117 if let Some(path) = explicit {
118 return AuditStoreChoice::Explicit(path);
119 }
120 if per_user_exists {
121 return AuditStoreChoice::PerUser(per_user);
122 }
123 if system_exists {
124 return AuditStoreChoice::System(PathBuf::from(PRODUCTION_DATABASE_PATH));
125 }
126 AuditStoreChoice::PerUser(per_user)
129}
130
131pub fn resolve_audit_store() -> AuditStoreChoice {
134 let explicit = std::env::var_os("SYSKNIFE_DATABASE_PATH").map(PathBuf::from);
135 let per_user = default_database_path();
136 let system = PathBuf::from(PRODUCTION_DATABASE_PATH);
137 let per_user_exists = per_user.exists();
138 let system_exists = system.exists();
139 choose_audit_store(explicit, per_user, per_user_exists, system_exists)
140}
141
142fn current_uid() -> u32 {
149 std::fs::read_to_string("/proc/self/status")
150 .ok()
151 .and_then(|s| {
152 s.lines()
153 .find(|l| l.starts_with("Uid:"))
154 .and_then(|l| l.split_whitespace().nth(1))
155 .and_then(|v| v.parse::<u32>().ok())
156 })
157 .unwrap_or(0)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::{
163 default_database_path, default_listen_uri, PRODUCTION_DATABASE_PATH, PRODUCTION_LISTEN_URI,
164 };
165 use std::sync::Mutex;
166
167 static ENV_LOCK: Mutex<()> = Mutex::new(());
168
169 #[test]
170 fn production_constants_are_absolute() {
171 assert!(PRODUCTION_LISTEN_URI.starts_with("unix:///run/"));
172 assert!(PRODUCTION_DATABASE_PATH.starts_with("/var/lib/"));
173 }
174
175 #[test]
176 fn database_env_var_wins() {
177 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
178 unsafe {
179 std::env::set_var("SYSKNIFE_DATABASE_PATH", "/explicit/path/db.sqlite");
180 }
181 let p = default_database_path();
182 unsafe {
183 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
184 }
185 assert_eq!(p.to_str(), Some("/explicit/path/db.sqlite"));
186 }
187
188 #[test]
189 fn database_xdg_state_used_when_set() {
190 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
191 unsafe {
192 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
193 std::env::set_var("XDG_STATE_HOME", "/xdg/state");
194 }
195 let p = default_database_path();
196 unsafe {
197 std::env::remove_var("XDG_STATE_HOME");
198 }
199 assert_eq!(p.to_str(), Some("/xdg/state/sysknife/daemon.sqlite"));
200 }
201
202 #[test]
203 fn database_falls_back_to_home_local_state() {
204 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
205 unsafe {
206 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
207 std::env::remove_var("XDG_STATE_HOME");
208 std::env::set_var("HOME", "/home/contributor");
209 }
210 let p = default_database_path();
211 unsafe {
212 std::env::remove_var("HOME");
213 }
214 assert_eq!(
215 p.to_str(),
216 Some("/home/contributor/.local/state/sysknife/daemon.sqlite")
217 );
218 }
219
220 #[test]
221 fn database_last_resort_is_production_path() {
222 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
223 let saved_home = std::env::var("HOME").ok();
225 unsafe {
226 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
227 std::env::remove_var("XDG_STATE_HOME");
228 std::env::remove_var("HOME");
229 }
230 let p = default_database_path();
231 unsafe {
232 if let Some(h) = saved_home {
233 std::env::set_var("HOME", h);
234 }
235 }
236 assert_eq!(p.to_str(), Some(PRODUCTION_DATABASE_PATH));
237 }
238
239 #[test]
240 fn listen_env_var_wins() {
241 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
242 unsafe {
243 std::env::set_var("SYSKNIFE_LISTEN_URI", "unix:///explicit.sock");
244 }
245 let u = default_listen_uri();
246 unsafe {
247 std::env::remove_var("SYSKNIFE_LISTEN_URI");
248 }
249 assert_eq!(u, "unix:///explicit.sock");
250 }
251
252 #[test]
253 fn listen_uses_xdg_runtime_dir_when_set() {
254 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
255 unsafe {
256 std::env::remove_var("SYSKNIFE_LISTEN_URI");
257 std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
258 }
259 let u = default_listen_uri();
260 unsafe {
261 std::env::remove_var("XDG_RUNTIME_DIR");
262 }
263 assert_eq!(u, "unix:///run/user/1000/sysknife/daemon.sock");
264 }
265
266 #[test]
267 fn listen_last_resort_is_per_uid_tmp() {
268 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
269 unsafe {
270 std::env::remove_var("SYSKNIFE_LISTEN_URI");
271 std::env::remove_var("XDG_RUNTIME_DIR");
272 }
273 let u = default_listen_uri();
274 assert!(u.starts_with("unix:///tmp/sysknife-"));
275 assert!(u.ends_with(".sock"));
276 }
277
278 use std::path::{Path, PathBuf};
283
284 #[test]
285 fn an_explicit_database_path_is_never_second_guessed() {
286 let choice = super::choose_audit_store(
287 Some(PathBuf::from("/srv/audit.sqlite")),
288 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
289 true,
290 true,
291 );
292 assert_eq!(choice.path(), Path::new("/srv/audit.sqlite"));
293 assert_eq!(
294 choice.note(),
295 None,
296 "an explicit choice needs no explanation"
297 );
298 }
299
300 #[test]
301 fn a_present_per_user_store_wins_over_the_system_one() {
302 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
303 let choice = super::choose_audit_store(None, per_user.clone(), true, true);
304 assert_eq!(choice.path(), per_user.as_path());
305 }
306
307 #[test]
310 fn the_system_store_is_used_when_no_per_user_store_exists() {
311 let choice = super::choose_audit_store(
312 None,
313 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
314 false,
315 true,
316 );
317 assert_eq!(choice.path(), Path::new(super::PRODUCTION_DATABASE_PATH));
318 let note = choice
319 .note()
320 .expect("reading another store must be announced");
321 assert!(
322 note.contains(super::PRODUCTION_DATABASE_PATH),
323 "note names the store: {note}"
324 );
325 }
326
327 #[test]
328 fn with_no_store_anywhere_the_per_user_path_is_reported() {
329 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
330 let choice = super::choose_audit_store(None, per_user.clone(), false, false);
331 assert_eq!(choice.path(), per_user.as_path());
332 assert_eq!(choice.note(), None);
333 }
334
335 #[test]
339 fn the_system_store_constant_matches_the_packaged_unit() {
340 let unit = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
341 .join("../../packaging/sysknife-daemon.service");
342 let contents = std::fs::read_to_string(&unit)
343 .unwrap_or_else(|e| panic!("read {}: {e}", unit.display()));
344 let expected = format!("SYSKNIFE_DATABASE_PATH={}", super::PRODUCTION_DATABASE_PATH);
345 assert!(
346 contents.contains(&expected),
347 "packaged unit must set {expected}; got:\n{contents}"
348 );
349 }
350}