voa_core/load_path.rs
1//! VOA [load path] handling.
2//!
3//! This module can produce a [`LoadPathList`] for both system and user mode.
4//!
5//! [load path]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#load-paths
6
7use std::path::{Path, PathBuf};
8
9use libc::geteuid;
10use log::{debug, trace};
11
12/// Load paths for "system mode" operation of VOA.
13/// Triplets of *path name*, a flag for *emphemerality*, and a flag for *writability*.
14const LOAD_PATHS_SYSTEM_MODE: &[(&str, bool, bool)] = &[
15 ("/etc/voa/", false, true),
16 ("/run/voa/", true, true),
17 ("/usr/local/share/voa/", false, false),
18 ("/usr/share/voa/", false, false),
19];
20
21/// A filter for [`LoadPath`]s.
22#[derive(Clone, Debug)]
23pub struct LoadPathFilter {
24 /// Whether a filtered [`LoadPath`] should be ephemeral.
25 pub ephemeral: bool,
26 /// Whether a filtered [`LoadPath`] should be writable.
27 pub writable: bool,
28}
29
30/// A VOA [load path].
31///
32/// [load path]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#load-paths
33#[derive(Clone, Debug, PartialEq)]
34pub struct LoadPath {
35 /// The file system path represented by the load path.
36 pub path: PathBuf,
37 ephemeral: bool,
38 writable: bool,
39}
40
41impl LoadPath {
42 /// Creates a new [`LoadPath`] from a [`PathBuf`].
43 ///
44 /// When setting the `ephemeral` flag to `true` the [`LoadPath`] is considered to be in an
45 /// ephemeral location, that is reset after reboot. When setting the `writable` flag to
46 /// `true` the [`LoadPath`] is considered writable (e.g. for masking symlinks).
47 pub(crate) fn new(path: impl Into<PathBuf>, ephemeral: bool, writable: bool) -> Self {
48 Self {
49 path: path.into(),
50 ephemeral,
51 writable,
52 }
53 }
54
55 /// Returns whether the [`LoadPath`] is considered to be ephemeral.
56 pub fn ephemeral(&self) -> bool {
57 self.ephemeral
58 }
59
60 /// Returns whether the [`LoadPath`] is considered to be writable.
61 pub fn writable(&self) -> bool {
62 self.writable
63 }
64
65 /// Returns the path.
66 pub fn path(&self) -> &Path {
67 &self.path
68 }
69
70 /// Checks whether a [`LoadPathFilter`] matches the properties of the [`LoadPath`].
71 ///
72 /// Returns `true` if the `filter` matches the properties of `self`, `false` otherwise.
73 pub fn matches_filter(&self, filter: &LoadPathFilter) -> bool {
74 self.ephemeral == filter.ephemeral && self.writable == filter.writable
75 }
76}
77
78impl From<&(&str, bool, bool)> for LoadPath {
79 fn from(value: &(&str, bool, bool)) -> Self {
80 Self {
81 path: value.0.into(),
82 ephemeral: value.1,
83 writable: value.2,
84 }
85 }
86}
87
88/// A list of load paths.
89///
90/// The order of the provided [`LoadPath`]s is important, as it defines the priority in which these
91/// paths will be explored. Paths that come first (smaller index) have a higher priority.
92#[derive(Debug)]
93pub struct LoadPathList(Vec<LoadPath>);
94
95impl LoadPathList {
96 /// Returns the system mode load paths as [`LoadPathList`].
97 ///
98 /// Contains a [`LoadPath`] each for the following directories:
99 ///
100 /// - `/etc/voa/`
101 /// - `/run/voa/`
102 /// - `/usr/local/share/voa/`
103 /// - `/usr/share/voa/`
104 pub(crate) fn load_path_list_system() -> LoadPathList {
105 let paths = LOAD_PATHS_SYSTEM_MODE.iter().map(Into::into).collect();
106
107 LoadPathList(paths)
108 }
109
110 /// Returns the user mode load paths as [`LoadPathList`].
111 ///
112 /// Contains a [`LoadPath`] each for the following directories:
113 ///
114 /// - `$XDG_CONFIG_HOME/voa/`
115 /// - the `./voa/` directory in each directory defined in `$XDG_CONFIG_DIRS`
116 /// - `$XDG_RUNTIME_DIR/voa/`
117 /// - `$XDG_DATA_HOME/voa/`
118 /// - the `./voa/` directory in each directory defined in `$XDG_DATA_DIRS`
119 pub(crate) fn load_path_list_user() -> LoadPathList {
120 let mut paths = vec![];
121
122 // Look into the XDG Base Directory Specification with qualifier "voa",
123 // organization "VOA", and application "VOA".
124 if let Some(proj_dirs) = directories::ProjectDirs::from("voa", "VOA", "VOA") {
125 // 1. $XDG_CONFIG_HOME/voa/
126 paths.push(LoadPath::new(
127 proj_dirs.config_dir().to_path_buf(),
128 false,
129 true,
130 ));
131
132 // 2. the ./voa/ directory in each directory defined in $XDG_CONFIG_DIRS
133 let xdg = xdg::BaseDirectories::with_prefix("voa");
134
135 xdg.get_config_dirs()
136 .into_iter()
137 .for_each(|dir| paths.push(LoadPath::new(dir, false, false)));
138
139 // 3. $XDG_RUNTIME_DIR/voa/
140 if let Some(runtime_dir) = proj_dirs.runtime_dir() {
141 paths.push(LoadPath::new(runtime_dir, true, true));
142 }
143
144 // 4. $XDG_DATA_HOME/voa/
145 paths.push(LoadPath::new(proj_dirs.data_dir(), false, false));
146
147 // 5. the ./voa/ directory in each directory defined in $XDG_DATA_DIRS
148 let mut data_dirs = xdg.get_data_dirs();
149
150 // If $XDG_DATA_DIRS is either not set or empty, a value equal to
151 // /usr/local/share/:/usr/share/ should be used.
152 if data_dirs.is_empty() {
153 data_dirs.push("/usr/local/share/voa/".into());
154 data_dirs.push("/usr/share/voa/".into());
155 }
156
157 data_dirs
158 .into_iter()
159 .for_each(|dir| paths.push(LoadPath::new(dir, false, false)));
160 }
161
162 LoadPathList(paths)
163 }
164
165 /// Returns the paths as [`LoadPathList`], depending on calling user.
166 ///
167 /// Checks the effective User ID of the calling process and
168 /// if the User ID is < `1000` returns the [system mode] load path list, else
169 /// the [user mode] load path list.
170 ///
171 /// # Safety
172 ///
173 /// Calls the unsafe [`libc::geteuid`] to determine the effective User ID of the process which
174 /// may panic.
175 /// A user is not guaranteed to exist after calling this function!
176 ///
177 /// [system mode]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#system-mode
178 /// [user mode]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#user-mode
179 pub fn from_effective_user() -> Self {
180 let euid = unsafe { geteuid() };
181 trace!("LoadPathList::from_effective_user called with process user id {euid}");
182
183 if euid < 1000 {
184 debug!("⤷ Using system mode load paths");
185 Self::load_path_list_system()
186 } else {
187 debug!("⤷ Using user mode load paths");
188 Self::load_path_list_user()
189 }
190 }
191
192 /// Returns a list of [`LoadPath`] references into which a provided [`LoadPath`] may point a
193 /// symlink.
194 ///
195 /// According to the VOA specification, symlinks from one `LoadPath` may only point to
196 /// locations in the same `LoadPath` or to locations in a `LoadPath` of **lower priority**.
197 ///
198 /// If `current` is not contained in `self`, an empty list is returned.
199 /// Otherwise, `current` and any [`LoadPath`] with lower priority will be returned.
200 ///
201 /// # Note
202 ///
203 /// Any _ephemeral_ [`LoadPath`] is excluded from the result.
204 pub(crate) fn legal_symlink_load_paths(&self, current: &LoadPath) -> Vec<&LoadPath> {
205 let mut legal = vec![];
206
207 // We're searching for "source" in self
208 let mut searching = true;
209
210 // This logic relies on `self.0` starting with the highest priority, with each following
211 // entry being of respectively lower priority than the previous one.
212 // -> We only start adding paths once we encounter `current`.
213 for path in &self.0 {
214 if searching {
215 if path.path == current.path {
216 searching = false;
217
218 if !path.ephemeral {
219 legal.push(path);
220 }
221 }
222 } else if !path.ephemeral {
223 legal.push(path);
224 }
225 }
226
227 legal
228 }
229
230 /// Returns a reference to the list of contained [`LoadPath`] instances.
231 pub fn paths(&self) -> &[LoadPath] {
232 &self.0
233 }
234
235 /// Returns a filtered list of the contained [`LoadPath`] instances
236 pub fn filter(&self, filter: &LoadPathFilter) -> Vec<&LoadPath> {
237 self.0
238 .iter()
239 .filter(|load_path| load_path.matches_filter(filter))
240 .collect()
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use rstest::rstest;
247
248 use super::*;
249
250 #[rstest]
251 #[case(
252 ("/etc/voa/", false, true),
253 &[("/etc/voa/", false, true),
254 ("/usr/local/share/voa/", false, false),
255 ("/usr/share/voa/", false, false)
256 ]
257 )]
258 #[case(
259 ("/run/voa/", true, true),
260 &[("/usr/local/share/voa/", false, false),
261 ("/usr/share/voa/", false, false)
262 ]
263 )]
264 #[case(
265 ("/usr/local/share/voa/", false, false),
266 &[("/usr/local/share/voa/", false, false),
267 ("/usr/share/voa/", false, false)
268 ]
269 )]
270 #[case(
271 ("/usr/share/voa/", false, false),
272 &[("/usr/share/voa/", false, false)]
273 )]
274 #[case(
275 ("/foo/bar/", false, false),
276 &[]
277 )]
278 fn test_legal_symlink_load_paths(
279 #[case] current: (&str, bool, bool),
280 #[case] expected: &[(&str, bool, bool)],
281 ) -> testresult::TestResult {
282 let load_path_list = LoadPathList(LOAD_PATHS_SYSTEM_MODE.iter().map(Into::into).collect());
283 let expected_paths: Vec<_> = expected.iter().map(Into::into).collect();
284
285 let legal = load_path_list.legal_symlink_load_paths(&(¤t).into());
286
287 assert_eq!(legal, expected_paths.iter().collect::<Vec<_>>());
288
289 Ok(())
290 }
291}