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            "/etc/systemd/system/",
211            "/run/systemd/system/",
212        ];
213
214        if !safe_loctions
215            .iter()
216            .any(|loc| file_path_str.starts_with(loc))
217        {
218            let err = SysdBaseError::InvalidPath(format!(
219                r#"The file "{}" not located in any of: {}"#,
220                file_path_str,
221                safe_loctions.join(" ")
222            ));
223
224            return Err(err);
225        }
226    }
227
228    info!("Valid path");
229
230    Ok(())
231}
232
233pub async fn write_on_disk(
234    //  user_session: bool,
235    file_path: &Path,
236    create_file: bool,
237    content: &str,
238) -> Result<u64, SysdBaseError> {
239    //safe_guard(&file_path.to_string_lossy(), user_session)?;
240    let bytes_written = match save_io(file_path, create_file, content).await {
241        Ok(b) => b,
242        Err(err) => {
243            if err.kind() == std::io::ErrorKind::PermissionDenied && getuid() != 0 {
244                write_with_priviledge(file_path, content).await
245            } else {
246                Err(err)?
247            }?
248        }
249    };
250    Ok(bytes_written)
251}
252
253async fn create_dir_all_with_priviledge(dir_path: &Path) -> Result<(), SysdBaseError> {
254    let prog_n_args = args!["pkexec", "mkdir", "-p", dir_path];
255    execute_command(None, &prog_n_args).await?;
256    Ok(())
257}
258
259pub async fn write_with_priviledge(file_path: &Path, text: &str) -> Result<u64, SysdBaseError> {
260    let prog_n_args = args!["pkexec", "tee", file_path];
261    let input = text.as_bytes();
262    execute_command(Some(input), &prog_n_args).await?;
263    Ok(input.len() as u64)
264}
265
266pub async fn execute_command(
267    input: Option<&[u8]>,
268    prog_n_args: &[&OsStr],
269) -> Result<(), SysdBaseError> {
270    let mut cmd = commander(prog_n_args, None);
271
272    let mut child = cmd
273        .stdin(Stdio::piped())
274        .stdout(Stdio::piped())
275        .stderr(Stdio::piped())
276        .spawn()
277        .map_err(|error: std::io::Error| SysdBaseError::create_command_error(&cmd, error))?;
278
279    let stdout = child
280        .stdout
281        .take()
282        .ok_or("Child did not have a handle to stdout")?;
283    //.expect("child did not have a handle to stdout");
284
285    let stderr = child
286        .stderr
287        .take()
288        .ok_or("Child did not have a handle to stderr")?;
289
290    if let Some(input) = input {
291        let mut child_stdin = child
292            .stdin
293            .take()
294            .ok_or("Unable to pass stdin to command")?;
295        child_stdin.write_all(input).await?;
296        drop(child_stdin);
297    }
298
299    let handle = tokio::spawn(async move {
300        let exit_status = child.wait().await?;
301        if exit_status.success() {
302            info!("Script executed with success");
303            return Ok(());
304        }
305
306        let code = exit_status
307            .code()
308            .inspect(|code| warn!("Subprocess exit code: {code:?}"))
309            .ok_or("Subprocess exit code: None")?;
310
311        let err = match code {
312            1 => {
313                #[cfg(feature = "flatpak")]
314                {
315                    SysdBaseError::CmdNoFreedesktopFlatpakPermission
316                }
317                #[cfg(not(feature = "flatpak"))]
318                {
319                    Err(format!("Subprocess exit code: {code}"))?
320                }
321            }
322            126 => SysdBaseError::NotAuthorized,
323            127 => SysdBaseError::NotAuthorizedAuthentificationDismissed,
324            _ => Err(format!("Subprocess exit code: {code}"))?,
325        };
326        Err(err)
327    });
328
329    let mut reader_out = BufReader::new(stdout).lines();
330    let mut reader_err = BufReader::new(stderr).lines();
331    debug!("Going to read out");
332
333    while let Some(line) = reader_out.next_line().await? {
334        info!("Script line: {}", line);
335    }
336
337    debug!("Going to read err");
338
339    while let Some(line) = reader_err.next_line().await? {
340        error!("Script line: {}", line);
341    }
342
343    debug!("Going to wait");
344
345    handle.await?
346}
347
348pub async fn save_io(
349    file_path: impl AsRef<Path>,
350    create: bool,
351    content: &str,
352) -> Result<u64, std::io::Error> {
353    let mut file = fs::OpenOptions::new()
354        .write(true)
355        .truncate(true)
356        .create(create)
357        .open(file_path)
358        .await?;
359
360    let test_bytes = content.as_bytes();
361
362    file.write_all(test_bytes).await?;
363    file.flush().await?;
364
365    let bytes_written = test_bytes.len();
366
367    Ok(bytes_written as u64)
368}
369
370pub const FLATPAK_SPAWN: &str = "flatpak-spawn";
371
372pub static INSIDE_FLATPAK: OnceLock<bool> = OnceLock::new();
373
374#[macro_export]
375macro_rules! inside_flatpak {
376    () => {
377        *INSIDE_FLATPAK.get_or_init(|| {
378            #[cfg(not(feature = "flatpak"))]
379            warn!("Not supposed to be called");
380
381            let in_flatpak = std::env::var("FLATPAK_ID").is_ok();
382
383            #[cfg(feature = "flatpak")]
384            if !in_flatpak {
385                warn!("Your run the flatpak compilation, but you aren't running inside a Flatpak");
386            }
387
388            in_flatpak
389        })
390    };
391}
392
393pub fn inside_flatpak() -> bool {
394    inside_flatpak!()
395}
396
397/*     pub fn args<I, S>(&mut self, args: I) -> &mut Command
398where
399    I: IntoIterator<Item = S>,
400    S: AsRef<OsStr>, */
401
402#[cfg(feature = "flatpak")]
403pub fn commander<I, S>(prog_n_args: I, environment_variables: Option<&[(&str, &str)]>) -> Command
404where
405    I: IntoIterator<Item = S>,
406    S: AsRef<OsStr>,
407{
408    if !inside_flatpak!() {
409        error!("Command call might not work because you are not running inside a Flatpak")
410    }
411
412    let mut cmd = Command::new(FLATPAK_SPAWN);
413    cmd.arg("--host");
414    cmd.args(prog_n_args);
415
416    if let Some(envs) = environment_variables {
417        for env in envs {
418            cmd.arg(format!("--env={}={}", env.0, env.1));
419        }
420    }
421
422    cmd
423}
424
425#[cfg(not(feature = "flatpak"))]
426pub fn commander<I, S>(prog_n_args: I, environment_variables: Option<&[(&str, &str)]>) -> Command
427where
428    I: IntoIterator<Item = S>,
429    S: AsRef<OsStr>,
430{
431    let mut it = prog_n_args.into_iter();
432    let mut cmd = Command::new(it.next().unwrap());
433
434    for arg in it {
435        cmd.arg(arg);
436    }
437
438    if let Some(envs) = environment_variables {
439        for env in envs {
440            cmd.env(env.0, env.1);
441        }
442    }
443
444    cmd
445}
446
447pub fn commander_blocking<I, S>(
448    prog_n_args: I,
449    environment_variables: Option<&[(&str, &str)]>,
450) -> std::process::Command
451where
452    I: IntoIterator<Item = S>,
453    S: AsRef<OsStr>,
454{
455    commander(prog_n_args, environment_variables).into_std()
456}
457
458pub fn test_flatpak_spawn() -> Result<(), io::Error> {
459    #[cfg(feature = "flatpak")]
460    {
461        info!("test_flatpak_spawn");
462        std::process::Command::new(FLATPAK_SPAWN)
463            .arg("--help")
464            .output()
465            .map(|_o| ())
466    }
467
468    #[cfg(not(feature = "flatpak"))]
469    Ok(())
470}
471
472/// To be able to acces the Flatpack mounted files.
473/// Limit to /usr for the least access principle
474pub fn flatpak_host_file_path(file_path: &str) -> PathBuf {
475    #[cfg(feature = "flatpak")]
476    {
477        if inside_flatpak!() && (file_path.starts_with("/usr") || file_path.starts_with("/etc")) {
478            let file_path = file_path.strip_prefix('/').unwrap_or(file_path);
479            PathBuf::from_iter(["/run/host", file_path])
480        } else {
481            PathBuf::from(&file_path)
482        }
483    }
484
485    #[cfg(not(feature = "flatpak"))]
486    PathBuf::from(file_path)
487}
488
489#[cfg(test)]
490mod test {
491    use super::*;
492    use test_base::init_logs;
493
494    pub fn flatpak_host_file_path_t(file_path: &str) -> PathBuf {
495        let file_path = if let Some(stripped) = file_path.strip_prefix('/') {
496            stripped
497        } else {
498            file_path
499        };
500        PathBuf::from_iter(["/run/host", file_path])
501    }
502
503    pub fn flatpak_host_file_path_t2(file_path: &str) -> PathBuf {
504        PathBuf::from("/run/host").join(file_path)
505    }
506
507    #[test]
508    fn test_fp() {
509        init_logs();
510
511        let src = PathBuf::from("/tmp");
512        let a = flatpak_host_file_path(&src.to_string_lossy());
513        warn!("{} exists {}", a.display(), a.exists());
514        warn!("{} exists {}", src.display(), src.exists());
515    }
516
517    #[test]
518    fn test_fp2() {
519        init_logs();
520
521        let src = PathBuf::from("/tmp");
522        let a = flatpak_host_file_path_t(&src.to_string_lossy());
523        warn!("{} exists {}", a.display(), a.exists());
524        warn!("{} exists {}", src.display(), src.exists());
525
526        let b = flatpak_host_file_path_t("test");
527        warn!("{} exists {}", b.display(), b.exists());
528
529        let b = flatpak_host_file_path_t("/test");
530        warn!("{} exists {}", b.display(), b.exists());
531
532        let b = flatpak_host_file_path_t2("/test");
533        warn!("{} exists {}", b.display(), b.exists());
534    }
535}