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