1use std::env::current_exe;
2use std::ffi::OsString;
3use std::fmt::Display;
4use std::fs::{self, Permissions};
5use std::io::{ErrorKind, Read, Write};
6use std::os::unix::fs::PermissionsExt;
7use std::path::{Path, PathBuf};
8
9use itertools::Itertools;
10
11use crate::install::files::process_parent::IdRes;
12use crate::install::RemoveStep;
13
14use super::init::PathCheckError;
15use super::{
16 init, BackupError, InstallError, InstallStep, Mode, RemoveError, RollbackError, RollbackStep,
17 Tense,
18};
19
20pub mod process_parent;
21
22#[derive(thiserror::Error, Debug)]
23pub enum MoveError {
24 #[error("could not find current users home dir")]
25 NoHome(
26 #[from]
27 #[source]
28 NoHomeError,
29 ),
30 #[error("none of the usual dirs for user binaries exist")]
31 UserDirNotAvailable,
32 #[error("none of the usual dirs for system binaries exist")]
33 SystemDirNotAvailable,
34 #[error("the path did not point to a binary")]
35 SourceNotFile,
36 #[error("could not move binary to install location")]
37 IO(#[source] std::io::Error),
38 #[error("overwrite is not set and there is already a file named {name} at {}", dir.display())]
39 TargetExists { name: String, dir: PathBuf },
40 #[error("{0}")]
41 TargetInUse(
42 #[from]
43 #[source]
44 TargetInUseError,
45 ),
46 #[error("could not check if already existing file is read only")]
47 CheckExistingFilePermissions(#[source] std::io::Error),
48 #[error("could not check if we are running from the target location")]
49 ResolveCurrentExe(#[source] std::io::Error),
50 #[error(
51 "could not check if already existing file is identical to what we are about to install"
52 )]
53 CompareFiles(#[source] CompareFileError),
54}
55
56fn system_dir() -> Option<PathBuf> {
57 let possible_paths: &[&'static Path] = &["/usr/bin/"].map(Path::new);
58
59 for path in possible_paths {
60 if path.parent().expect("never root").is_dir() {
61 return Some(path.to_path_buf());
62 }
63 }
64 None
65}
66
67#[derive(Debug, thiserror::Error)]
68#[error("Home directory not known")]
69pub struct NoHomeError;
70
71fn user_dir() -> Result<Option<PathBuf>, NoHomeError> {
72 let possible_paths: &[&'static Path] = &[".local/bin"].map(Path::new);
73
74 for relative in possible_paths {
75 let path = home::home_dir().ok_or(NoHomeError)?.join(relative);
76 if path.parent().expect("never root").is_dir() {
77 return Ok(Some(path));
78 }
79 }
80 Ok(None)
81}
82
83pub(crate) struct Move {
84 name: OsString,
85 source: PathBuf,
86 pub target: PathBuf,
87}
88
89impl InstallStep for Move {
90 fn describe_detailed(&self, tense: Tense) -> String {
91 let verb = match tense {
92 Tense::Past => "Copied",
93 Tense::Questioning => "Copy",
94 Tense::Future => "Will copy",
95 Tense::Active => "Copying",
96 };
97 let name = self.name.to_string_lossy();
98 let source = self
99 .source
100 .parent()
101 .expect("path points to file, so has parent")
102 .display();
103 let target = self
104 .target
105 .parent()
106 .expect("path points to file, so has parent")
107 .display();
108 format!(
109 "{verb} executable `{name}`{}\n| from:\n|\t{source}\n| to:\n|\t{target}",
110 tense.punct()
111 )
112 }
113
114 fn describe(&self, tense: Tense) -> String {
115 let verb = match tense {
116 Tense::Past => "Copied",
117 Tense::Questioning => "Copy",
118 Tense::Future => "Will copy",
119 Tense::Active => "Copying",
120 };
121 let name = self.name.to_string_lossy();
122 let target = self
123 .target
124 .parent()
125 .expect("path points to file, so has parent")
126 .display();
127 format!(
128 "{verb} executable `{name}` to:\n|\t{target}{}",
129 tense.punct()
130 )
131 }
132
133 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
134 let rollback_step = if self.target.is_file() {
135 let target_content = fs::read(&self.target)
136 .map_err(BackupError::Read)
137 .map_err(InstallError::Backup)?;
138
139 let mut backup = tempfile::tempfile()
140 .map_err(BackupError::Create)
141 .map_err(InstallError::Backup)?;
142 backup
143 .write_all(&target_content)
144 .map_err(BackupError::Write)
145 .map_err(InstallError::Backup)?;
146
147 Box::new(MoveBack {
148 backup,
149 target: self.target.clone(),
150 }) as Box<dyn RollbackStep>
151 } else {
152 Box::new(Remove {
153 target: self.target.clone(),
154 }) as Box<dyn RollbackStep>
155 };
156
157 match std::fs::copy(&self.source, &self.target) {
158 Err(e) => Err(InstallError::CopyExeError(e)),
159 Ok(_) => Ok(Some(rollback_step)),
160 }
161 }
162}
163
164#[derive(Debug, thiserror::Error)]
165pub enum MoveBackError {
166 #[error("Could not read backup from file")]
167 ReadingBackup(#[source] std::io::Error),
168 #[error("Could not write to target")]
169 WritingToTarget(#[source] std::io::Error),
170}
171
172struct MoveBack {
173 backup: std::fs::File,
176 target: PathBuf,
177}
178
179impl RollbackStep for MoveBack {
180 fn perform(&mut self) -> Result<(), RollbackError> {
181 let mut buf = Vec::new();
182 self.backup
183 .read_to_end(&mut buf)
184 .map_err(MoveBackError::ReadingBackup)
185 .map_err(RollbackError::MovingBack)?;
186 fs::write(&self.target, buf)
187 .map_err(MoveBackError::WritingToTarget)
188 .map_err(RollbackError::MovingBack)
189 }
190
191 fn describe(&self, tense: Tense) -> String {
192 let verb = match tense {
193 Tense::Past => "Moved",
194 Tense::Questioning => "Move",
195 Tense::Active => "Moving",
196 Tense::Future => "Will move",
197 };
198 format!(
199 "{verb} back the file that was origonally at the install location{}",
200 tense.punct()
201 )
202 }
203}
204
205struct SetRootOwner {
206 path: PathBuf,
207}
208
209impl InstallStep for SetRootOwner {
210 fn describe(&self, tense: Tense) -> String {
211 let verb = match tense {
212 Tense::Past | Tense::Questioning => "Set",
213 Tense::Active => "Setting",
214 Tense::Future => "Will set",
215 };
216 format!("{verb} executables owner to root{}", tense.punct())
217 }
218
219 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
220 const ROOT: u32 = 0;
221 std::os::unix::fs::chown(&self.path, Some(ROOT), Some(ROOT))
222 .map_err(InstallError::SetRootOwner)?;
223 Ok(None)
224 }
225}
226
227#[derive(Debug, thiserror::Error)]
228pub enum SetReadOnlyError {
229 #[error("Could not get current permissions for file")]
230 GetPermissions(#[source] std::io::Error),
231 #[error("Could not set permissions for file")]
232 SetPermissions(#[source] std::io::Error),
233}
234
235struct MakeReadExecOnly {
236 path: PathBuf,
237}
238
239impl InstallStep for MakeReadExecOnly {
240 fn describe(&self, tense: Tense) -> String {
241 let verb = match tense {
242 Tense::Past => "Made",
243 Tense::Questioning => "Make",
244 Tense::Future => "Will make",
245 Tense::Active => "Making",
246 };
247 format!(
248 "{verb} the executable read and execute only{}",
249 tense.punct()
250 )
251 }
252
253 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
254 use std::os::unix::fs::PermissionsExt;
255
256 let org_permissions = fs::metadata(&self.path)
257 .map_err(SetReadOnlyError::GetPermissions)?
258 .permissions();
259 let mut permissions = org_permissions.clone();
260 permissions.set_mode(0o555);
261 fs::set_permissions(&self.path, permissions).map_err(SetReadOnlyError::SetPermissions)?;
262 Ok(Some(Box::new(RestorePermissions {
263 path: self.path.clone(),
264 org_permissions,
265 })))
266 }
267}
268
269struct RestorePermissions {
270 path: PathBuf,
271 org_permissions: Permissions,
272}
273
274impl RollbackStep for RestorePermissions {
275 fn perform(&mut self) -> Result<(), RollbackError> {
276 match fs::set_permissions(&self.path, self.org_permissions.clone()) {
277 Ok(()) => Ok(()),
278 Err(io) if io.kind() == std::io::ErrorKind::NotFound => {
281 tracing::warn!("Could not restore permissions, file is not there");
282 Ok(())
283 }
284 Err(other) => Err(RollbackError::RestoringPermissions(other)),
285 }
286 }
287
288 fn describe(&self, tense: Tense) -> String {
289 let verb = match tense {
290 Tense::Past => "Restored",
291 Tense::Active => "Restoring",
292 Tense::Questioning => "Restore",
293 Tense::Future => "Will Restore",
294 };
295 format!("{verb} executables previous permissions{}", tense.punct())
296 }
297}
298
299struct FilesAlreadyInstalled {
300 target: PathBuf,
301}
302
303impl InstallStep for FilesAlreadyInstalled {
304 fn describe(&self, tense: Tense) -> String {
305 match tense {
306 Tense::Past => "this binary was already installed in the target location",
307 Tense::Questioning | Tense::Future | Tense::Active => {
308 "this binary is already installed in the target location"
309 }
310 }
311 .to_owned()
312 }
313
314 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
315 Ok(None)
316 }
317
318 fn describe_detailed(&self, tense: Tense) -> String {
319 format!(
320 "{}\n|\ttarget location: {}",
321 self.describe(tense),
322 self.target.display()
323 )
324 }
325
326 fn options(&self) -> Option<super::StepOptions> {
327 None }
329}
330
331type Steps = Vec<Box<dyn InstallStep>>;
332pub(crate) fn move_files(
333 source: PathBuf,
334 mode: Mode,
335 run_as: Option<&str>,
336 overwrite_existing: bool,
337 init_systems: &[init::System],
338) -> Result<(Steps, PathBuf), MoveError> {
339 let dir = match mode {
340 Mode::User => user_dir()?.ok_or(MoveError::UserDirNotAvailable)?,
341 Mode::System => system_dir().ok_or(MoveError::SystemDirNotAvailable)?,
342 };
343
344 let file_name = source
345 .file_name()
346 .ok_or(MoveError::SourceNotFile)?
347 .to_owned();
348 let target = dir.join(&file_name);
349 let current_exe = current_exe().map_err(MoveError::ResolveCurrentExe)?;
350
351 if target.is_file()
352 && content_identical(&target, ¤t_exe).map_err(MoveError::CompareFiles)?
353 {
354 let step = FilesAlreadyInstalled {
355 target: target.clone(),
356 };
357 return Ok((vec![Box::new(step) as Box<dyn InstallStep>], target));
358 } else if target.is_file() && !overwrite_existing {
359 return Err(MoveError::TargetExists {
360 name: file_name.to_string_lossy().to_string(),
361 dir,
362 });
363 }
364
365 let mut steps = Vec::new();
366 if let Some(make_removable) = make_removable_if_needed(&target)? {
367 steps.push(make_removable);
368 }
369
370 let disable_steps = disable_if_running(&target, init_systems, mode, run_as)?;
371 steps.extend(disable_steps);
372
373 steps.extend([
374 Box::new(Move {
375 name: file_name,
376 source,
377 target: target.clone(),
378 }) as Box<dyn InstallStep>,
379 Box::new(MakeReadExecOnly {
380 path: target.clone(),
381 }),
382 ]);
383 if let Mode::System = mode {
384 steps.push(Box::new(SetRootOwner {
385 path: target.clone(),
386 }));
387 }
388
389 Ok((steps, target))
390}
391
392#[derive(Debug, thiserror::Error)]
393pub enum CompareFileError {
394 #[error("opening the new file")]
395 IoNew(#[source] std::io::Error),
396 #[error("opening existing file")]
397 IoExisting(#[source] std::io::Error),
398
399 #[error("checking length of new file")]
400 LenNew(#[source] std::io::Error),
401 #[error("checking length of existing file")]
402 LenExisting(#[source] std::io::Error),
403
404 #[error("reading the new file")]
405 ReadNew(#[source] std::io::Error),
406 #[error("reading the existing file")]
407 ReadExisting(#[source] std::io::Error),
408}
409
410fn content_identical(existing: &Path, new: &Path) -> Result<bool, CompareFileError> {
411 use CompareFileError as E;
412
413 if existing == new {
414 return Ok(true);
415 }
416
417 let mut existing = fs::File::open(existing).map_err(E::IoExisting)?;
418 let mut new = fs::File::open(new).map_err(E::IoNew)?;
419
420 if existing.metadata().map_err(E::LenExisting)?.len()
421 != new.metadata().map_err(E::LenNew)?.len()
422 {
423 return Ok(false);
424 }
425
426 let mut e_buf = [0; 4096];
427 let mut n_buf = [0; 4096];
428
429 loop {
430 let e_read = read_into_buf(&mut existing, &mut e_buf).map_err(E::ReadExisting)?;
431 let n_read = read_into_buf(&mut new, &mut n_buf).map_err(E::ReadNew)?;
432 if e_read != n_read {
433 return Ok(false);
434 } else if e_read.len() == 4096 {
435 return Ok(true);
436 }
437 }
438}
439
440fn read_into_buf<'a>(
444 file: &mut fs::File,
445 buf: &'a mut [u8],
446) -> Result<&'a mut [u8], std::io::Error> {
447 let mut total_read = 0;
448 let mut free_buf = &mut buf[..];
449 loop {
450 let n = file.read(free_buf)?;
451 total_read += n;
452 if n == 0 || n == free_buf.len() {
453 break;
454 } else {
455 free_buf = &mut free_buf[n..];
456 }
457 }
458
459 Ok(&mut buf[..total_read])
460}
461
462struct MakeRemovable(PathBuf);
463
464fn make_removable_if_needed(target: &Path) -> Result<Option<Box<dyn InstallStep>>, MoveError> {
465 let permissions = match fs::metadata(target) {
466 Ok(meta) => meta,
467 Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
468 Err(e) => return Err(MoveError::CheckExistingFilePermissions(e)),
469 }
470 .permissions();
471
472 Ok(if permissions.readonly() {
473 let step = MakeRemovable(target.to_owned());
474 let step = Box::new(step) as Box<dyn InstallStep>;
475 Some(step)
476 } else {
477 None
478 })
479}
480
481impl InstallStep for MakeRemovable {
482 fn describe(&self, tense: Tense) -> String {
483 let verb = match tense {
484 Tense::Past => "Made",
485 Tense::Questioning => "Make",
486 Tense::Future => "Will make",
487 Tense::Active => "Making",
488 };
489 format!(
490 "{verb} the file taking up the install location removable{}",
491 tense.punct()
492 )
493 }
494
495 fn describe_detailed(&self, tense: Tense) -> String {
496 let verb = match tense {
497 Tense::Past => "Made",
498 Tense::Questioning => "Make",
499 Tense::Future => "Will make",
500 Tense::Active => "Making",
501 };
502 format!("A different read only file is taking up the install location. {verb} it removable by making it writable{}\n| file:\n|\t{}", tense.punct(), self.0.display())
503 }
504
505 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
506 let org_permissions = fs::metadata(&self.0)
507 .map_err(SetReadOnlyError::GetPermissions)?
508 .permissions();
509 let mut permissions = org_permissions.clone();
510 permissions.set_mode(0o600);
511 fs::set_permissions(&self.0, permissions).map_err(SetReadOnlyError::SetPermissions)?;
512 Ok(Some(Box::new(RestorePermissions {
513 path: self.0.clone(),
514 org_permissions,
515 })))
516 }
517}
518
519#[derive(Debug, thiserror::Error)]
520pub enum TargetInUseError {
521 NoParent,
522 ResolvePath(
523 #[from]
524 #[source]
525 PathCheckError,
526 ),
527 Parents(Vec<PathBuf>),
528 CouldNotDisable(
529 #[from]
530 #[source]
531 DisableError,
532 ),
533}
534
535#[derive(Debug, thiserror::Error)]
536pub enum DisableError {
537 #[error(transparent)]
538 SystemD(#[from] init::systemd::DisableError),
539 #[error(transparent)]
540 Cron(#[from] init::cron::disable::Error),
541}
542
543impl Display for TargetInUseError {
544 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545 match self {
546 TargetInUseError::NoParent => {
547 writeln!(f, "There is already a file at the install location. It can not be replaced as it is running. We have no information on how it was started as it has no parent")
548 }
549 TargetInUseError::ResolvePath(_) => {
550 writeln!(f, "There is already a file at the install location. It can not be replaced as it is running. While it has a parent we failed to get information about it")
551 }
552 TargetInUseError::Parents(tree) => {
553 let tree = tree.iter().map(|p| p.display().to_string());
554 let tree: String = Itertools::intersperse(tree, " -> ".to_string()).collect();
555 writeln!(f, "There is already a file at the install location. It can not be replaced as it is running.\n\tThe process tree that started that:\n\t`{tree}`\nIn this tree the arrow means left started the right process")
556 }
557 TargetInUseError::CouldNotDisable(err) => {
558 writeln!(
559 f,
560 "The file we need to replace is in use by a running service however we could not disable that service. {err}"
561 )
562 }
563 }
564 }
565}
566
567fn disable_if_running(
568 target: &Path,
569 init_systems: &[init::System],
570 mode: Mode,
571 run_as: Option<&str>,
572) -> Result<Vec<Box<dyn InstallStep>>, TargetInUseError> {
573 let mut steps = Vec::new();
574
575 for parent_info in process_parent::list(target, init_systems)? {
576 match parent_info {
577 IdRes::ParentIsInit { init, pid } => {
578 steps.append(&mut init.disable_steps(target, pid, mode, run_as)?);
579 }
580 IdRes::NoParent => return Err(TargetInUseError::NoParent)?,
581 IdRes::ParentNotInit { parents, pid } => {
582 steps.push(process_parent::kill_old_steps(pid, parents));
583 }
584 }
585 }
586
587 Ok(steps)
588}
589
590#[derive(thiserror::Error, Debug)]
591pub enum DeleteError {
592 #[error("could not find current users home dir")]
593 NoHome(
594 #[from]
595 #[source]
596 NoHomeError,
597 ),
598 #[error("none of the usual dirs for user binaries exist")]
599 UserDirNotAvailable,
600 #[error("none of the usual dirs for system binaries exist")]
601 SystemDirNotAvailable,
602 #[error("the path did not point to a binary")]
603 SourceNotFile,
604 #[error("could not move binary to install location")]
605 IO(#[source] std::io::Error),
606 #[error("Could not get the current executable's location")]
607 GetExeLocation(#[source] std::io::Error),
608 #[error("May only uninstall the currently running binary, running: {running} installed: {installed}")]
609 ExeNotInstalled {
610 running: PathBuf,
611 installed: PathBuf,
612 },
613}
614
615pub(crate) struct Remove {
616 target: PathBuf,
617}
618
619impl RemoveStep for Remove {
620 fn describe(&self, tense: Tense) -> String {
621 let verb = match tense {
622 Tense::Past => "Removed",
623 Tense::Questioning => "Remove",
624 Tense::Future => "Will remove",
625 Tense::Active => "Removing",
626 };
627 let bin = self
628 .target
629 .file_name()
630 .expect("In fn exe_path we made sure target is a file")
631 .to_string_lossy();
632 format!("{verb} installed executable `{bin}`{}", tense.punct())
633 }
634
635 fn describe_detailed(&self, tense: Tense) -> String {
636 let verb = match tense {
637 Tense::Past => "Removed",
638 Tense::Questioning => "Remove",
639 Tense::Future => "Will remove",
640 Tense::Active => "Removing",
641 };
642 let bin = self
643 .target
644 .file_name()
645 .expect("In fn exe_path we made sure target is a file")
646 .to_string_lossy();
647 let dir = self
648 .target
649 .parent()
650 .expect("There is always a parent on linux")
651 .display();
652 format!(
653 "{verb} installed executable `{bin}`{} Is installed at:\n|\t{dir}",
654 tense.punct()
655 )
656 }
657
658 fn perform(&mut self) -> Result<(), RemoveError> {
659 std::fs::remove_file(&self.target)
660 .map_err(DeleteError::IO)
661 .map_err(Into::into)
662 }
663}
664
665pub(crate) fn remove_files(installed: PathBuf) -> Remove {
666 Remove { target: installed }
667}