rootless_run/
fakeroot.rs

1//! A rootless backend that uses [fakeroot].
2//!
3//! [fakeroot]: https://man.archlinux.org/man/fakeroot.1
4
5#[cfg(doc)]
6use std::path::Path;
7use std::{
8    fmt::Display,
9    path::PathBuf,
10    process::{Command, Output},
11};
12
13use log::debug;
14
15use crate::{Error, RootlessBackend, RootlessOptions, utils::get_command};
16
17/// The `fakeroot` `-l`/`--lib` option.
18const ARG_LIBRARY: &str = "--lib";
19/// The `fakeroot` `--faked` option.
20const ARG_FAKED: &str = "--faked";
21/// The `fakeroot` `-s` option.
22const ARG_SAVE_FILE: &str = "-s";
23/// The `fakeroot` `-i` option.
24const ARG_LOAD_FILE: &str = "-i";
25/// The `fakeroot` `--unknown-is-real` option.
26const ARG_UNKNOWN_IS_REAL: &str = "--unknown-is-real";
27/// The `fakeroot` `-b` option.
28const ARG_FD: &str = "-b";
29/// The separator between `fakeroot` options and the command run with `fakeroot` (`--`).
30const ARG_SEPARATOR: &str = "--";
31
32/// Options for [fakeroot].
33///
34/// [fakeroot]: https://man.archlinux.org/man/fakeroot.1
35#[derive(Clone, Debug, Default, Eq, PartialEq)]
36pub struct FakerootOptions {
37    /// An alternative wrapper library.
38    ///
39    /// Corresponds to `fakeroot`'s `-l`/`--lib` option.
40    pub library: Option<PathBuf>,
41
42    /// An alternative binary to use as `faked`.
43    ///
44    /// Corresponds to `fakeroot`'s `--faked` option.
45    pub faked: Option<PathBuf>,
46
47    /// A file to save the environment to.
48    ///
49    /// Corresponds to `fakeroot`'s `-s` option.
50    pub save_file: Option<PathBuf>,
51
52    /// A file to load a previous environment from.
53    ///
54    /// Corresponds to `fakeroot`'s `-i` option.
55    pub load_file: Option<PathBuf>,
56
57    /// Whether to use the real ownership of files.
58    ///
59    /// Corresponds to `fakeroot`'s `-u`/`--unknown-is-real` option.
60    pub unknown_is_real: bool,
61
62    /// The minimum file descriptor number for TCP connections.
63    ///
64    /// Corresponds to `fakeroot`'s `-b` option.
65    pub fd: Option<usize>,
66}
67
68impl Display for FakerootOptions {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(f, "{}", self.to_vec().join(" "))
71    }
72}
73
74impl RootlessOptions for FakerootOptions {
75    /// Returns the options as a [`String`] [`Vec`].
76    ///
77    /// # Notes
78    ///
79    /// All [`PathBuf`] options are represented using [`Path::to_string_lossy`].
80    ///
81    /// The last entry is always the string `"--"`.
82    fn to_vec(&self) -> Vec<String> {
83        let mut options = Vec::new();
84        if let Some(option) = self.library.as_ref() {
85            options.push(ARG_LIBRARY.to_string());
86            options.push(option.to_string_lossy().to_string());
87        }
88        if let Some(option) = self.faked.as_ref() {
89            options.push(ARG_FAKED.to_string());
90            options.push(option.to_string_lossy().to_string());
91        }
92        if let Some(option) = self.save_file.as_ref() {
93            options.push(ARG_SAVE_FILE.to_string());
94            options.push(option.to_string_lossy().to_string());
95        }
96        if let Some(option) = self.load_file.as_ref() {
97            options.push(ARG_LOAD_FILE.to_string());
98            options.push(option.to_string_lossy().to_string());
99        }
100        if self.unknown_is_real {
101            options.push(ARG_UNKNOWN_IS_REAL.to_string());
102        }
103        if let Some(option) = self.fd {
104            options.push(ARG_FD.to_string());
105            options.push(option.to_string());
106        }
107        options.push(ARG_SEPARATOR.to_string());
108
109        options
110    }
111}
112
113/// A rootless backend for running commands using [fakeroot].
114///
115/// [fakeroot]: https://man.archlinux.org/man/fakeroot.1
116#[derive(Debug)]
117pub struct FakerootBackend(FakerootOptions);
118
119impl RootlessBackend<FakerootOptions> for FakerootBackend {
120    type Err = Error;
121
122    /// Creates a new [`FakerootBackend`].
123    fn new(options: FakerootOptions) -> Self {
124        debug!("Creating a new fakeroot backend with options: \"{options}\"");
125        Self(options)
126    }
127
128    /// Returns the [`FakerootOptions`] used by the [`FakerootBackend`].
129    fn options(&self) -> &FakerootOptions {
130        &self.0
131    }
132
133    /// Runs a command using [fakeroot] and returns its [`Output`].
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if
138    ///
139    /// - the [fakeroot] command cannot be found,
140    /// - or the provided `command` cannot be run using [fakeroot].
141    ///
142    /// [fakeroot]: https://man.archlinux.org/man/fakeroot.1
143    fn run(&self, cmd: &[&str]) -> Result<Output, Error> {
144        let command_name = get_command("fakeroot")?;
145        let mut command = Command::new(command_name);
146
147        // Add all options to fakeroot as arguments.
148        if let Some(option) = self.0.library.as_ref() {
149            command.arg(ARG_LIBRARY);
150            command.arg(option);
151        }
152        if let Some(option) = self.0.faked.as_ref() {
153            command.arg(ARG_FAKED);
154            command.arg(option);
155        }
156        if let Some(option) = self.0.save_file.as_ref() {
157            command.arg(ARG_SAVE_FILE);
158            command.arg(option);
159        }
160        if let Some(option) = self.0.load_file.as_ref() {
161            command.arg(ARG_LOAD_FILE);
162            command.arg(option);
163        }
164        if self.0.unknown_is_real {
165            command.arg(ARG_UNKNOWN_IS_REAL);
166        }
167        if let Some(option) = self.0.fd {
168            command.arg(ARG_FD);
169            command.arg(option.to_string());
170        }
171        command.arg(ARG_SEPARATOR);
172
173        // Add input cmd as arguments to fakeroot.
174        for command_component in cmd.iter() {
175            command.arg(command_component);
176        }
177
178        debug!("Run rootless command: {command:?}");
179
180        command
181            .output()
182            .map_err(|source| crate::Error::CommandExec {
183                command: format!("{command:?}"),
184                source,
185            })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use rstest::rstest;
192
193    use super::*;
194
195    /// Ensures that [`FakerootOptions`] are constructed as [`String`] [`Vec`] properly.
196    #[rstest]
197    #[case::all_options(
198        FakerootOptions{
199            library: Some(PathBuf::from("custom-lib")),
200            faked: Some(PathBuf::from("custom-faked")),
201            save_file: Some(PathBuf::from("/custom/save/file")),
202            load_file: Some(PathBuf::from("/custom/load/file")),
203            unknown_is_real: true,
204            fd: Some(1024),
205        },
206        vec![
207            ARG_LIBRARY.to_string(),
208            "custom-lib".to_string(),
209            ARG_FAKED.to_string(),
210            "custom-faked".to_string(),
211            ARG_SAVE_FILE.to_string(),
212            "/custom/save/file".to_string(),
213            ARG_LOAD_FILE.to_string(),
214            "/custom/load/file".to_string(),
215            ARG_UNKNOWN_IS_REAL.to_string(),
216            ARG_FD.to_string(),
217            "1024".to_string(),
218            ARG_SEPARATOR.to_string(),
219        ]
220    )]
221    #[case::default_options(FakerootOptions::default(), vec![ARG_SEPARATOR.to_string()])]
222    fn fakeroot_options_to_vec(#[case] options: FakerootOptions, #[case] to_vec: Vec<String>) {
223        assert_eq!(options.to_vec(), to_vec);
224    }
225
226    /// Ensures that [`FakerootOptions`] is returned from [`FakerootBackend::options`].
227    #[test]
228    fn fakeroot_backend_options() {
229        let backend = FakerootBackend::new(FakerootOptions::default());
230        assert_eq!(backend.options(), &FakerootOptions::default());
231    }
232}