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(&(&current).into());
286
287        assert_eq!(legal, expected_paths.iter().collect::<Vec<_>>());
288
289        Ok(())
290    }
291}