1use std::path::PathBuf;
7
8pub mod cgfs;
9pub mod effector;
10pub mod history;
11pub mod journal;
12pub mod lock;
13pub mod notify;
14pub mod policy;
15pub mod report;
16pub mod resolve;
17pub mod sampler;
18pub mod service;
19pub mod systemd;
20pub mod types;
21
22pub use effector::{Applied, Effector};
23pub use journal::Journal;
24pub use policy::PolicyEngine;
25pub use sampler::Sampler;
26pub use systemd::SystemdUser;
27pub use types::{Action, Intervention, Level, ProcInfo, PsiSource, Sample, Target};
28
29pub fn guard_file(name: &str) -> Option<PathBuf> {
40 let state = dirs::state_dir();
41 let runtime = if state.is_none() {
42 private_runtime_dir()
43 } else {
44 None
45 };
46 guard_file_from(state, runtime, name)
47}
48
49fn private_runtime_dir() -> Option<PathBuf> {
52 use std::os::unix::fs::MetadataExt;
53 static WARNED: std::sync::Once = std::sync::Once::new();
54 let dir = dirs::runtime_dir()?;
55 let uid = crate::process::current_uid();
56 match std::fs::metadata(&dir) {
57 Ok(m) if runtime_dir_is_private(m.uid(), m.mode(), uid) => Some(dir),
58 Ok(m) => {
59 WARNED.call_once(|| {
60 tracing::warn!(
61 "ignoring XDG_RUNTIME_DIR {}: owner uid {} mode {:o}, expected owner {} \
62 and not group or world writable",
63 dir.display(),
64 m.uid(),
65 m.mode() & 0o7777,
66 uid
67 )
68 });
69 None
70 }
71 Err(e) => {
72 WARNED.call_once(|| tracing::warn!("ignoring XDG_RUNTIME_DIR {}: {e}", dir.display()));
73 None
74 }
75 }
76}
77
78fn runtime_dir_is_private(owner: u32, mode: u32, uid: u32) -> bool {
80 owner == uid && mode & 0o022 == 0
81}
82
83fn guard_file_from(
84 state: Option<PathBuf>,
85 runtime: Option<PathBuf>,
86 name: &str,
87) -> Option<PathBuf> {
88 state.or(runtime).map(|d| d.join("rlm").join(name))
89}
90
91pub fn try_journal_path() -> Option<PathBuf> {
95 guard_file("guard-journal.jsonl")
96}
97
98pub fn journal_path() -> PathBuf {
102 try_journal_path().unwrap_or_default()
103}
104
105pub fn lock_path() -> Option<PathBuf> {
109 guard_file("rlm-guard.lock")
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn guard_files_prefer_the_state_dir() {
118 assert_eq!(
119 guard_file_from(Some("/s".into()), Some("/r".into()), "rlm-guard.lock"),
120 Some(PathBuf::from("/s/rlm/rlm-guard.lock"))
121 );
122 }
123
124 #[test]
125 fn guard_files_fall_back_to_the_runtime_dir_not_tmp() {
126 assert_eq!(
127 guard_file_from(None, Some("/run/user/1000".into()), "guard-journal.jsonl"),
128 Some(PathBuf::from("/run/user/1000/rlm/guard-journal.jsonl"))
129 );
130 assert_eq!(guard_file_from(None, None, "guard-journal.jsonl"), None);
131 }
132
133 #[test]
134 fn runtime_dir_is_used_only_when_private_to_us() {
135 assert!(runtime_dir_is_private(1000, 0o40700, 1000));
136 assert!(runtime_dir_is_private(1000, 0o40750, 1000));
137 assert!(!runtime_dir_is_private(0, 0o40700, 1000));
139 assert!(!runtime_dir_is_private(1000, 0o40770, 1000));
141 assert!(!runtime_dir_is_private(1000, 0o40702, 1000));
142 assert!(!runtime_dir_is_private(1000, 0o41777, 1000));
143 }
144
145 #[test]
146 fn journal_reader_path_is_empty_without_a_dir() {
147 let p = try_journal_path().unwrap_or_default();
149 assert!(!p.starts_with("/tmp"), "{p:?}");
150 assert!(Journal::read_entries(&PathBuf::new(), "boot").is_empty());
151 }
152}