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 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 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 file_path: &Path,
237 create_file: bool,
238 content: &str,
239) -> Result<u64, SysdBaseError> {
240 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 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#[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
473pub 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}