Skip to main content

sysd_manager_base/
file.rs

1use std::ffi::{OsStr, OsString};
2use std::path::Component;
3use std::process::Stdio;
4use std::{
5    error::Error,
6    io,
7    path::{Path, PathBuf},
8    sync::OnceLock,
9};
10use tokio::fs;
11use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
12use tokio::process::Command;
13
14use tokio::task::JoinError;
15use tracing::debug;
16#[allow(unused_imports)]
17use tracing::{error, info, warn};
18
19use crate::getuid;
20
21#[macro_export]
22macro_rules! args {
23    ($($a:expr),*) => {
24        [
25            $(AsRef::<OsStr>::as_ref(&$a),)*
26        ]
27    }
28}
29
30#[macro_export]
31macro_rules! vs {
32    ($($a:expr),*) => {
33        [
34            $(AsRef::<String>::as_ref(&$a),)*
35        ]
36    }
37}
38
39#[derive(Debug)]
40pub enum SysdBaseError {
41    CmdNoFreedesktopFlatpakPermission,
42    CommandCallError(
43        OsString,
44        Vec<OsString>,
45        Vec<(OsString, Option<OsString>)>,
46        io::Error,
47    ),
48    Custom(String),
49    IoError(io::Error),
50    NotAuthorizedAuthentificationDismissed,
51    NotAuthorized,
52    Tokio(JoinError),
53    InvalidPath(String),
54}
55
56impl SysdBaseError {
57    pub(crate) fn create_command_error(command: &Command, error: std::io::Error) -> Self {
58        let std_command = command.as_std();
59        let program = std_command.get_program().to_os_string();
60        let envs: Vec<(OsString, Option<OsString>)> = std_command
61            .get_envs()
62            .map(|(k, v)| (k.to_os_string(), v.map(|s| s.to_os_string())))
63            .collect();
64        let arg: Vec<OsString> = std_command.get_args().map(|s| s.to_os_string()).collect();
65
66        SysdBaseError::CommandCallError(program, arg, envs, error)
67    }
68}
69
70impl From<&str> for SysdBaseError {
71    fn from(value: &str) -> Self {
72        value.to_string().into()
73    }
74}
75
76impl From<String> for SysdBaseError {
77    fn from(value: String) -> Self {
78        SysdBaseError::Custom(value)
79    }
80}
81
82impl From<std::io::Error> for SysdBaseError {
83    fn from(value: std::io::Error) -> Self {
84        SysdBaseError::IoError(value)
85    }
86}
87
88impl From<JoinError> for SysdBaseError {
89    fn from(value: JoinError) -> Self {
90        SysdBaseError::Tokio(value)
91    }
92}
93
94pub fn determine_drop_in_path_dir(
95    unit_name: &str,
96    runtime: bool,
97    user_session: bool,
98) -> Result<String, Box<dyn Error + 'static>> {
99    let path = match (runtime, user_session) {
100        (true, false) => format!("/run/systemd/system/{}.d", unit_name),
101        (false, false) => format!("/etc/systemd/system/{}.d", unit_name),
102        (true, true) => {
103            let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
104                .unwrap_or_else(|_| format!("/run/user/{}", getuid()));
105
106            format!("{runtime_dir}/systemd/user/{}.d", unit_name)
107        }
108        (false, true) => {
109            let home_dir = std::env::home_dir().ok_or(Box::<dyn Error>::from(
110                "No HOME found to create drop-in".to_string(),
111            ))?;
112            format!(
113                "{}/.config/systemd/user/{}.d",
114                home_dir.display(),
115                unit_name
116            )
117        }
118    };
119    Ok(path)
120}
121
122pub fn create_drop_in_path_file(
123    unit_name: &str,
124    runtime: bool,
125    user_session: bool,
126    file_name: &str,
127) -> Result<String, Box<dyn Error + 'static>> {
128    let path_dir = determine_drop_in_path_dir(unit_name, runtime, user_session)?;
129
130    let path = format!("{path_dir}/{file_name}.conf");
131
132    info!(
133        "Creating drop-in path for unit: {}, runtime: {}, user: {} -> path {}",
134        unit_name, runtime, user_session, path
135    );
136    Ok(path)
137}
138
139pub async fn create_drop_in_io(
140    file_path_str: &str,
141    content: &str,
142    user_session: bool,
143) -> Result<(), SysdBaseError> {
144    //FIXME add security
145    if file_path_str.contains("../") {
146        let err = std::io::Error::new(
147            std::io::ErrorKind::InvalidData,
148            r#"The "../" pattern is not supported""#,
149        );
150
151        return Err(err)?;
152    }
153
154    let file_path = PathBuf::from(file_path_str);
155
156    if file_path.components().any(|c| c == Component::ParentDir) {
157        let err = std::io::Error::new(
158            std::io::ErrorKind::InvalidData,
159            r#"The "../" pattern is not supported for file path"#,
160        );
161
162        return Err(err)?;
163    }
164
165    path_safe_guard(user_session, file_path_str)?;
166
167    let unit_drop_in_dir = file_path.parent().ok_or(std::io::Error::new(
168        std::io::ErrorKind::InvalidData,
169        format!("Parent dir of file {:?} is invalid", file_path_str),
170    ))?;
171
172    if !unit_drop_in_dir.exists() {
173        info!("Creating dir {}", unit_drop_in_dir.display());
174        match fs::create_dir_all(&unit_drop_in_dir).await {
175            Ok(_) => {}
176            Err(err) => {
177                if err.kind() == std::io::ErrorKind::PermissionDenied && getuid() != 0 {
178                    create_dir_all_with_priviledge(unit_drop_in_dir).await
179                } else {
180                    Err(err)?
181                }?
182            }
183        }
184    }
185
186    //Save content
187    info!("Creating file {}", file_path.display());
188    let bytes_written = write_on_disk(&file_path, true, content).await?;
189
190    info!(
191        "{bytes_written} bytes writen on File {}",
192        file_path.to_string_lossy()
193    );
194    Ok(())
195}
196
197pub fn path_safe_guard(user_session: bool, file_path_str: &str) -> Result<(), SysdBaseError> {
198    if file_path_str.contains("../") {
199        let err = SysdBaseError::InvalidPath(format!(
200            r#"The file path "{}" not absoluated"#,
201            file_path_str,
202        ));
203
204        return Err(err);
205    }
206
207    if !user_session {
208        let safe_loctions = [
209            "/usr/lib/systemd/system/",
210            "/lib/systemd/system/",
211            "/etc/systemd/system/",
212            "/run/systemd/system/",
213        ];
214
215        if !safe_loctions
216            .iter()
217            .any(|loc| file_path_str.starts_with(loc))
218        {
219            let err = SysdBaseError::InvalidPath(format!(
220                r#"The file "{}" not located in any of: {}"#,
221                file_path_str,
222                safe_loctions.join(" ")
223            ));
224
225            return Err(err);
226        }
227    }
228
229    info!("Valid path");
230
231    Ok(())
232}
233
234pub async fn write_on_disk(
235    //  user_session: bool,
236    file_path: &Path,
237    create_file: bool,
238    content: &str,
239) -> Result<u64, SysdBaseError> {
240    //safe_guard(&file_path.to_string_lossy(), user_session)?;
241    let bytes_written = match save_io(file_path, create_file, content).await {
242        Ok(b) => b,
243        Err(err) => {
244            if err.kind() == std::io::ErrorKind::PermissionDenied && getuid() != 0 {
245                write_with_priviledge(file_path, content).await
246            } else {
247                Err(err)?
248            }?
249        }
250    };
251    Ok(bytes_written)
252}
253
254async fn create_dir_all_with_priviledge(dir_path: &Path) -> Result<(), SysdBaseError> {
255    let prog_n_args = args!["pkexec", "mkdir", "-p", dir_path];
256    execute_command(None, &prog_n_args).await?;
257    Ok(())
258}
259
260pub async fn write_with_priviledge(file_path: &Path, text: &str) -> Result<u64, SysdBaseError> {
261    let prog_n_args = args!["pkexec", "tee", file_path];
262    let input = text.as_bytes();
263    execute_command(Some(input), &prog_n_args).await?;
264    Ok(input.len() as u64)
265}
266
267pub async fn execute_command(
268    input: Option<&[u8]>,
269    prog_n_args: &[&OsStr],
270) -> Result<(), SysdBaseError> {
271    let mut cmd = commander(prog_n_args, None);
272
273    let mut child = cmd
274        .stdin(Stdio::piped())
275        .stdout(Stdio::piped())
276        .stderr(Stdio::piped())
277        .spawn()
278        .map_err(|error: std::io::Error| SysdBaseError::create_command_error(&cmd, error))?;
279
280    let stdout = child
281        .stdout
282        .take()
283        .ok_or("Child did not have a handle to stdout")?;
284    //.expect("child did not have a handle to stdout");
285
286    let stderr = child
287        .stderr
288        .take()
289        .ok_or("Child did not have a handle to stderr")?;
290
291    if let Some(input) = input {
292        let mut child_stdin = child
293            .stdin
294            .take()
295            .ok_or("Unable to pass stdin to command")?;
296        child_stdin.write_all(input).await?;
297        drop(child_stdin);
298    }
299
300    let handle = tokio::spawn(async move {
301        let exit_status = child.wait().await?;
302        if exit_status.success() {
303            info!("Script executed with success");
304            return Ok(());
305        }
306
307        let code = exit_status
308            .code()
309            .inspect(|code| warn!("Subprocess exit code: {code:?}"))
310            .ok_or("Subprocess exit code: None")?;
311
312        let err = match code {
313            1 => {
314                #[cfg(feature = "flatpak")]
315                {
316                    SysdBaseError::CmdNoFreedesktopFlatpakPermission
317                }
318                #[cfg(not(feature = "flatpak"))]
319                {
320                    Err(format!("Subprocess exit code: {code}"))?
321                }
322            }
323            126 => SysdBaseError::NotAuthorized,
324            127 => SysdBaseError::NotAuthorizedAuthentificationDismissed,
325            _ => Err(format!("Subprocess exit code: {code}"))?,
326        };
327        Err(err)
328    });
329
330    let mut reader_out = BufReader::new(stdout).lines();
331    let mut reader_err = BufReader::new(stderr).lines();
332    debug!("Going to read out");
333
334    while let Some(line) = reader_out.next_line().await? {
335        debug!("Script line: {}", line);
336    }
337
338    debug!("Going to read err");
339
340    while let Some(line) = reader_err.next_line().await? {
341        error!("Script line: {}", line);
342    }
343
344    debug!("Going to wait");
345
346    handle.await?
347}
348
349pub async fn save_io(
350    file_path: impl AsRef<Path>,
351    create: bool,
352    content: &str,
353) -> Result<u64, std::io::Error> {
354    let mut file = fs::OpenOptions::new()
355        .write(true)
356        .truncate(true)
357        .create(create)
358        .open(file_path)
359        .await?;
360
361    let test_bytes = content.as_bytes();
362
363    file.write_all(test_bytes).await?;
364    file.flush().await?;
365
366    let bytes_written = test_bytes.len();
367
368    Ok(bytes_written as u64)
369}
370
371pub const FLATPAK_SPAWN: &str = "flatpak-spawn";
372
373pub static INSIDE_FLATPAK: OnceLock<bool> = OnceLock::new();
374
375#[macro_export]
376macro_rules! inside_flatpak {
377    () => {
378        *INSIDE_FLATPAK.get_or_init(|| {
379            #[cfg(not(feature = "flatpak"))]
380            warn!("Not supposed to be called");
381
382            let in_flatpak = std::env::var("FLATPAK_ID").is_ok();
383
384            #[cfg(feature = "flatpak")]
385            if !in_flatpak {
386                warn!("Your run the flatpak compilation, but you aren't running inside a Flatpak");
387            }
388
389            in_flatpak
390        })
391    };
392}
393
394pub fn inside_flatpak() -> bool {
395    inside_flatpak!()
396}
397
398/*     pub fn args<I, S>(&mut self, args: I) -> &mut Command
399where
400    I: IntoIterator<Item = S>,
401    S: AsRef<OsStr>, */
402
403#[cfg(feature = "flatpak")]
404pub fn commander<I, S>(prog_n_args: I, environment_variables: Option<&[(&str, &str)]>) -> Command
405where
406    I: IntoIterator<Item = S>,
407    S: AsRef<OsStr>,
408{
409    if !inside_flatpak!() {
410        error!("Command call might not work because you are not running inside a Flatpak")
411    }
412
413    let mut cmd = Command::new(FLATPAK_SPAWN);
414    cmd.arg("--host");
415    cmd.args(prog_n_args);
416
417    if let Some(envs) = environment_variables {
418        for env in envs {
419            cmd.arg(format!("--env={}={}", env.0, env.1));
420        }
421    }
422
423    cmd
424}
425
426#[cfg(not(feature = "flatpak"))]
427pub fn commander<I, S>(prog_n_args: I, environment_variables: Option<&[(&str, &str)]>) -> Command
428where
429    I: IntoIterator<Item = S>,
430    S: AsRef<OsStr>,
431{
432    let mut it = prog_n_args.into_iter();
433    let mut cmd = Command::new(it.next().unwrap());
434
435    for arg in it {
436        cmd.arg(arg);
437    }
438
439    if let Some(envs) = environment_variables {
440        for env in envs {
441            cmd.env(env.0, env.1);
442        }
443    }
444
445    cmd
446}
447
448pub fn commander_blocking<I, S>(
449    prog_n_args: I,
450    environment_variables: Option<&[(&str, &str)]>,
451) -> std::process::Command
452where
453    I: IntoIterator<Item = S>,
454    S: AsRef<OsStr>,
455{
456    commander(prog_n_args, environment_variables).into_std()
457}
458
459pub fn test_flatpak_spawn() -> Result<(), io::Error> {
460    #[cfg(feature = "flatpak")]
461    {
462        info!("test_flatpak_spawn");
463        std::process::Command::new(FLATPAK_SPAWN)
464            .arg("--help")
465            .output()
466            .map(|_o| ())
467    }
468
469    #[cfg(not(feature = "flatpak"))]
470    Ok(())
471}
472
473/// To be able to acces the Flatpack mounted files.
474/// Limit to /usr for the least access principle
475pub fn flatpak_host_file_path(file_path: &str) -> PathBuf {
476    #[cfg(feature = "flatpak")]
477    {
478        if inside_flatpak!() && (file_path.starts_with("/usr") || file_path.starts_with("/etc")) {
479            let file_path = file_path.strip_prefix('/').unwrap_or(file_path);
480            PathBuf::from_iter(["/run/host", file_path])
481        } else {
482            PathBuf::from(&file_path)
483        }
484    }
485
486    #[cfg(not(feature = "flatpak"))]
487    PathBuf::from(file_path)
488}
489
490#[cfg(test)]
491mod test {
492    use super::*;
493    use test_base::init_logs;
494
495    pub fn flatpak_host_file_path_t(file_path: &str) -> PathBuf {
496        let file_path = if let Some(stripped) = file_path.strip_prefix('/') {
497            stripped
498        } else {
499            file_path
500        };
501        PathBuf::from_iter(["/run/host", file_path])
502    }
503
504    pub fn flatpak_host_file_path_t2(file_path: &str) -> PathBuf {
505        PathBuf::from("/run/host").join(file_path)
506    }
507
508    #[test]
509    fn test_fp() {
510        init_logs();
511
512        let src = PathBuf::from("/tmp");
513        let a = flatpak_host_file_path(&src.to_string_lossy());
514        warn!("{} exists {}", a.display(), a.exists());
515        warn!("{} exists {}", src.display(), src.exists());
516    }
517
518    #[test]
519    fn test_fp2() {
520        init_logs();
521
522        let src = PathBuf::from("/tmp");
523        let a = flatpak_host_file_path_t(&src.to_string_lossy());
524        warn!("{} exists {}", a.display(), a.exists());
525        warn!("{} exists {}", src.display(), src.exists());
526
527        let b = flatpak_host_file_path_t("test");
528        warn!("{} exists {}", b.display(), b.exists());
529
530        let b = flatpak_host_file_path_t("/test");
531        warn!("{} exists {}", b.display(), b.exists());
532
533        let b = flatpak_host_file_path_t2("/test");
534        warn!("{} exists {}", b.display(), b.exists());
535    }
536}