1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use crate::domain::{AbsentPolicy, CommitOwnership, InstallPlan, MemberId, Ownership};
7use crate::error::{Error, RecoveryError, RecoveryResult, Result};
8use crate::integrity::hash_file;
9use crate::lock::MutationLock;
10use crate::stage::PreparedTransaction;
11
12static NEXT_BACKUP_NONCE: AtomicU64 = AtomicU64::new(0);
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum TransactionDisposition {
24 Committed,
26 RolledBack,
28 RecoveryRequired,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CleanupDisposition {
44 Cleaned,
46 RetainedForRecovery,
48 DeferredToProcessExit,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum PostCommitFailurePolicy {
62 KeepInstalled,
64 RollBack,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74#[non_exhaustive]
75pub enum FailurePhase {
76 Lock,
78 Ownership,
80 StageRevalidation,
82 Backup,
84 Commit,
86 PostCommit,
88 Rollback,
90 Finalize,
92}
93
94impl FailurePhase {
95 pub fn as_str(&self) -> &'static str {
97 match self {
98 Self::Lock => "lock",
99 Self::Ownership => "ownership",
100 Self::StageRevalidation => "stage-revalidation",
101 Self::Backup => "backup",
102 Self::Commit => "commit",
103 Self::PostCommit => "post-commit",
104 Self::Rollback => "rollback",
105 Self::Finalize => "finalize",
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136#[non_exhaustive]
137pub enum FailureCategory {
138 LockContention,
142 OwnershipConflict,
146 Verification,
151 Filesystem,
155 InvalidInput,
159 Injected,
165 PostCommitCheck,
170}
171
172impl FailureCategory {
173 pub fn as_str(&self) -> &'static str {
180 match self {
181 Self::LockContention => "lock-contention",
182 Self::OwnershipConflict => "ownership-conflict",
183 Self::Verification => "verification",
184 Self::Filesystem => "filesystem",
185 Self::InvalidInput => "invalid-input",
186 Self::Injected => "injected",
187 Self::PostCommitCheck => "post-commit-check",
188 }
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct FailureReport {
204 phase: FailurePhase,
205 member: Option<MemberId>,
206 category: FailureCategory,
207 detail: String,
208}
209
210impl FailureReport {
211 pub(crate) fn new(
212 phase: FailurePhase,
213 member: Option<MemberId>,
214 category: FailureCategory,
215 detail: impl Into<String>,
216 ) -> Self {
217 let mut detail = detail.into();
218 if detail.len() > 512 {
220 let boundary = detail
221 .char_indices()
222 .map(|(index, _)| index)
223 .take_while(|index| *index <= 512)
224 .last()
225 .unwrap_or(0);
226 detail.truncate(boundary);
227 }
228 Self {
229 phase,
230 member,
231 category,
232 detail,
233 }
234 }
235
236 pub fn phase(&self) -> FailurePhase {
238 self.phase
239 }
240
241 pub fn member(&self) -> Option<&MemberId> {
243 self.member.as_ref()
244 }
245
246 pub fn category(&self) -> FailureCategory {
248 self.category
249 }
250
251 pub fn detail(&self) -> &str {
253 &self.detail
254 }
255}
256
257pub(crate) fn report_for_error(
258 phase: FailurePhase,
259 member: Option<MemberId>,
260 error: &Error,
261) -> FailureReport {
262 let (category, detail) = match error {
263 Error::UpdateInProgress { lock } => (
264 FailureCategory::LockContention,
265 format!("lock contention at {}", lock.display()),
266 ),
267 Error::DestinationConflict { destination } => (
268 FailureCategory::OwnershipConflict,
269 format!("destination conflict at {}", destination.display()),
270 ),
271 Error::VerificationFailed(m) => (FailureCategory::Verification, m.clone()),
272 Error::CandidateExecution(m) => (FailureCategory::Verification, m.clone()),
273 #[cfg(test)]
274 Error::Injected(m) => (FailureCategory::Injected, m.clone()),
275 Error::InvalidInput(m) => (FailureCategory::InvalidInput, m.clone()),
276 Error::UnknownMember(m) => (FailureCategory::InvalidInput, format!("unknown member {m}")),
277 Error::Io { operation, source } => (
278 FailureCategory::Filesystem,
279 format!("{operation}: {source}"),
280 ),
281 };
282 FailureReport::new(phase, member, category, detail)
283}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct TransactionReceipt {
299 product: crate::domain::ProductId,
300 release: crate::domain::ReleaseId,
301 disposition: TransactionDisposition,
302 rollback_performed: bool,
303 rollback_verified: bool,
304 cleanup: CleanupDisposition,
305 recovery_path: Option<PathBuf>,
306 failure: Option<FailureReport>,
307 rollback_failure: Option<FailureReport>,
308 post_commit_failure: Option<FailureReport>,
309}
310
311impl TransactionReceipt {
312 pub fn product(&self) -> &crate::domain::ProductId {
314 &self.product
315 }
316
317 pub fn release(&self) -> &crate::domain::ReleaseId {
319 &self.release
320 }
321
322 pub fn disposition(&self) -> TransactionDisposition {
324 self.disposition
325 }
326
327 pub fn rollback_performed(&self) -> bool {
329 self.rollback_performed
330 }
331
332 pub fn rollback_verified(&self) -> bool {
341 self.rollback_verified
342 }
343
344 pub fn cleanup(&self) -> CleanupDisposition {
346 self.cleanup
347 }
348
349 pub fn recovery_path(&self) -> Option<&Path> {
357 self.recovery_path.as_deref()
358 }
359
360 pub fn failure(&self) -> Option<&FailureReport> {
368 self.failure.as_ref()
369 }
370
371 pub fn rollback_failure(&self) -> Option<&FailureReport> {
376 self.rollback_failure.as_ref()
377 }
378
379 pub fn post_commit_failure(&self) -> Option<&FailureReport> {
381 self.post_commit_failure.as_ref()
382 }
383}
384
385#[derive(Debug, Clone, PartialEq, Eq)]
386#[cfg_attr(not(test), allow(dead_code))]
387pub(crate) enum CommitFault {
388 LockCreation,
389 Backup(MemberId),
390 BeforeFirstCommit,
391 Commit(MemberId),
392 CommitThenRollback(MemberId, MemberId),
393 Finalize,
394 PostCommitRollback,
395}
396
397#[derive(Debug)]
398struct BackupEntry {
399 member: MemberId,
400 destination: PathBuf,
401 backup: Option<PathBuf>,
402}
403
404type PostCommitCheck<'a> = (
405 PostCommitFailurePolicy,
406 Box<dyn FnOnce() -> std::result::Result<(), String> + 'a>,
407);
408
409impl PreparedTransaction {
410 pub(crate) fn commit_inner(
411 self,
412 ownership: CommitOwnership<'_>,
413 verified_digests: &HashMap<MemberId, [u8; 32]>,
414 fault: Option<CommitFault>,
415 post_commit: Option<PostCommitCheck<'_>>,
416 stale_recovery: Option<&dyn crate::lock::StaleLockVerifier>,
417 ) -> RecoveryResult<TransactionReceipt> {
418 #[cfg(test)]
419 if fault == Some(CommitFault::LockCreation) {
420 return Err(Error::injected("injected lock creation failure").into());
421 }
422 let preflight = classify_all(&self, &ownership)?;
425 let mut lock = match stale_recovery {
426 None => MutationLock::acquire(
430 self.plan.installation_root(),
431 self.plan.product(),
432 self.plan.release(),
433 )
434 .map_err(RecoveryError::from),
435 Some(verifier) => MutationLock::acquire_with_recovery(
436 self.plan.installation_root(),
437 self.plan.product(),
438 self.plan.release(),
439 verifier,
440 ),
441 }?;
442 if let Err(error) = revalidate_ownership_locked(&self, &ownership, &preflight) {
445 let report = report_for_error(FailurePhase::Ownership, None, &error);
446 let verified = true;
447 return finish_failure_no_mutation(self, lock, verified, report);
448 }
449 if let Err((member, error)) = revalidate_staged_under_lock(&self, verified_digests) {
452 let report = report_for_error(FailurePhase::StageRevalidation, member, &error);
453 return finish_failure_no_mutation(self, lock, true, report);
454 }
455 let backup_root = match create_backup_directory(self.plan.installation_root()) {
456 Ok(p) => p,
457 Err(error) => {
458 let report = report_for_error(FailurePhase::Backup, None, &error);
459 return finish_failure_no_mutation(self, lock, true, report);
460 }
461 };
462 let mut entries = Vec::with_capacity(self.plan.artifacts().len());
463 if let Err((member, error)) = self.backup_members(&mut entries, &backup_root, fault.clone())
464 {
465 let phase = FailurePhase::Backup;
466 let report = report_for_error(phase, member, &error);
467 let (verified, rollback_failure) =
468 restore_entries(&entries, &HashSet::new(), fault.clone());
469 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
470 }
471 #[cfg(test)]
472 if fault == Some(CommitFault::BeforeFirstCommit) {
473 let error = Error::injected("injected pre-commit failure");
474 let report = report_for_error(FailurePhase::Commit, None, &error);
475 let (verified, rollback_failure) =
476 restore_entries(&entries, &HashSet::new(), fault.clone());
477 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
478 }
479
480 let mut committed = HashSet::new();
481 let members = self.plan.artifacts().iter().cloned().collect::<Vec<_>>();
482 for member in members {
483 #[cfg(test)]
484 let commit_should_fail = fault.as_ref().is_some_and(|candidate| match candidate {
485 CommitFault::Commit(id) | CommitFault::CommitThenRollback(id, _) => {
486 id == member.id()
487 }
488 _ => false,
489 });
490 #[cfg(test)]
491 if commit_should_fail {
492 let error = Error::injected(format!("injected commit failure at {}", member.id()));
493 let report =
494 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
495 let (verified, rollback_failure) =
496 restore_entries(&entries, &committed, fault.clone());
497 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
498 }
499 let destination = match self.plan.destination(member.id()) {
500 Ok(destination) => destination,
501 Err(error) => {
502 let report =
503 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
504 let (verified, rollback_failure) =
505 restore_entries(&entries, &committed, fault.clone());
506 return finish_failure(
507 self,
508 lock,
509 backup_root,
510 verified,
511 rollback_failure,
512 report,
513 );
514 }
515 };
516 if let Err(error) = require_ready_parent(&destination, self.plan.installation_root()) {
517 let report =
518 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
519 let (verified, rollback_failure) =
520 restore_entries(&entries, &committed, fault.clone());
521 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
522 }
523 let staged = match self.staged_path(member.id()) {
524 Ok(staged) => staged,
525 Err(error) => {
526 let report =
527 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
528 let (verified, rollback_failure) =
529 restore_entries(&entries, &committed, fault.clone());
530 return finish_failure(
531 self,
532 lock,
533 backup_root,
534 verified,
535 rollback_failure,
536 report,
537 );
538 }
539 };
540 if let Err(source) = fs::rename(&staged, &destination) {
541 let error = Error::io("replacing destination from prepared stage", source);
542 let report =
543 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
544 let (verified, rollback_failure) =
545 restore_entries(&entries, &committed, fault.clone());
546 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
547 }
548 committed.insert(member.id().clone());
549 }
550
551 let mut post_commit_failure = None;
552 if let Some((policy, check)) = post_commit {
553 let checked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(check));
554 let failure_detail = match checked {
555 Ok(Ok(())) => None,
556 Ok(Err(detail)) => Some(detail),
557 Err(_) => Some("post-commit check panicked".to_string()),
558 };
559 if let Some(detail) = failure_detail {
560 let report = FailureReport::new(
561 FailurePhase::PostCommit,
562 None,
563 FailureCategory::PostCommitCheck,
564 detail,
565 );
566 match policy {
567 PostCommitFailurePolicy::KeepInstalled => {
568 post_commit_failure = Some(report);
569 }
570 PostCommitFailurePolicy::RollBack => {
571 let rollback_fault = if fault == Some(CommitFault::PostCommitRollback) {
572 Some(CommitFault::CommitThenRollback(
573 self.plan
574 .artifacts()
575 .iter()
576 .next()
577 .expect("non-empty artifact set")
578 .id()
579 .clone(),
580 self.plan
581 .artifacts()
582 .iter()
583 .next()
584 .expect("non-empty artifact set")
585 .id()
586 .clone(),
587 ))
588 } else {
589 fault.clone()
590 };
591 let (verified, rollback_failure) =
592 restore_entries(&entries, &committed, rollback_fault);
593 let mut receipt = finish_failure(
594 self,
595 lock,
596 backup_root,
597 verified,
598 rollback_failure,
599 report.clone(),
600 )?;
601 receipt.post_commit_failure = Some(report);
602 return Ok(receipt);
603 }
604 }
605 }
606 }
607
608 if fault == Some(CommitFault::Finalize) {
609 lock.preserve();
610 let report = FailureReport::new(
611 FailurePhase::Finalize,
612 None,
613 FailureCategory::Injected,
614 "injected finalize failure",
615 );
616 return Ok(TransactionReceipt {
617 product: self.plan.product().clone(),
618 release: self.plan.release().clone(),
619 disposition: TransactionDisposition::Committed,
620 rollback_performed: false,
621 rollback_verified: false,
622 cleanup: CleanupDisposition::RetainedForRecovery,
623 recovery_path: Some(backup_root),
624 failure: Some(report),
625 rollback_failure: None,
626 post_commit_failure,
627 });
628 }
629 match finalize_backup_set(&self.plan, &backup_root, &entries) {
635 Ok(FinalizeOutcome::Cleaned) => Ok(TransactionReceipt {
636 product: self.plan.product().clone(),
637 release: self.plan.release().clone(),
638 disposition: TransactionDisposition::Committed,
639 rollback_performed: false,
640 rollback_verified: false,
641 cleanup: CleanupDisposition::Cleaned,
642 recovery_path: None,
643 failure: None,
644 rollback_failure: None,
645 post_commit_failure,
646 }),
647 Ok(FinalizeOutcome::Deferred) => Ok(TransactionReceipt {
648 product: self.plan.product().clone(),
649 release: self.plan.release().clone(),
650 disposition: TransactionDisposition::Committed,
651 rollback_performed: false,
652 rollback_verified: false,
653 cleanup: CleanupDisposition::DeferredToProcessExit,
654 recovery_path: None,
657 failure: None,
658 rollback_failure: None,
659 post_commit_failure,
660 }),
661 Err(source) => {
662 lock.preserve();
663 let error = Error::io("removing backup set after commit", source);
664 let report = report_for_error(FailurePhase::Finalize, None, &error);
665 Ok(TransactionReceipt {
666 product: self.plan.product().clone(),
667 release: self.plan.release().clone(),
668 disposition: TransactionDisposition::Committed,
669 rollback_performed: false,
670 rollback_verified: false,
671 cleanup: CleanupDisposition::RetainedForRecovery,
672 recovery_path: Some(backup_root),
673 failure: Some(report),
674 rollback_failure: None,
675 post_commit_failure,
676 })
677 }
678 }
679 }
680
681 fn backup_members(
682 &self,
683 entries: &mut Vec<BackupEntry>,
684 backup_root: &Path,
685 fault: Option<CommitFault>,
686 ) -> std::result::Result<(), (Option<MemberId>, Error)> {
687 #[cfg(not(test))]
688 let _ = &fault;
689 let members = self.plan.artifacts().iter().cloned().collect::<Vec<_>>();
690 for member in members {
691 #[cfg(test)]
692 if fault == Some(CommitFault::Backup(member.id().clone())) {
693 return Err((
694 Some(member.id().clone()),
695 Error::injected(format!("injected backup failure at {}", member.id())),
696 ));
697 }
698 let destination = self
699 .plan
700 .destination(member.id())
701 .map_err(|e| (Some(member.id().clone()), e))?;
702 revalidate_destination(&destination, self.plan.installation_root())
703 .map_err(|e| (Some(member.id().clone()), e))?;
704 self.plan
708 .revalidate_current_executable()
709 .map_err(|e| (Some(member.id().clone()), e))?;
710 let backup = match fs::symlink_metadata(&destination) {
711 Ok(_) => {
712 let path = backup_root.join(member.destination());
713 if let Some(parent) = path.parent() {
714 fs::create_dir_all(parent).map_err(|source| {
715 (
716 Some(member.id().clone()),
717 Error::io("creating backup directory", source),
718 )
719 })?;
720 }
721 fs::rename(&destination, &path).map_err(|source| {
722 (
723 Some(member.id().clone()),
724 Error::io("backing up existing destination", source),
725 )
726 })?;
727 Some(path)
728 }
729 Err(source) if source.kind() == std::io::ErrorKind::NotFound => None,
730 Err(source) => {
731 return Err((
732 Some(member.id().clone()),
733 Error::io("reading destination before backup", source),
734 ));
735 }
736 };
737 entries.push(BackupEntry {
738 member: member.id().clone(),
739 destination,
740 backup,
741 });
742 }
743 Ok(())
744 }
745}
746
747fn classify_all(
748 txn: &PreparedTransaction,
749 ownership: &CommitOwnership<'_>,
750) -> Result<HashMap<MemberId, Ownership>> {
751 let mut out = HashMap::new();
752 for member in txn.artifacts().iter() {
753 let destination = txn.destination(member.id())?;
754 out.insert(
755 member.id().clone(),
756 ownership.verifier.verify(member.id(), &destination),
757 );
758 }
759 Ok(out)
760}
761
762#[cfg_attr(not(windows), allow(dead_code))]
764enum FinalizeOutcome {
765 Cleaned,
767 Deferred,
769}
770
771fn finalize_backup_set(
781 plan: &InstallPlan,
782 backup_root: &Path,
783 entries: &[BackupEntry],
784) -> std::io::Result<FinalizeOutcome> {
785 let running_image = plan
788 .current_executable()
789 .and_then(|_| entries.iter().find_map(|entry| entry.backup.clone()));
790 #[cfg(windows)]
791 if let Some(backup) = running_image {
792 self_replace::self_delete_at(&backup).map_err(|source| {
796 std::io::Error::other(format!(
797 "scheduling removal of the replaced running image: {source}"
798 ))
799 })?;
800 return match fs::remove_dir(backup_root) {
803 Ok(()) => Ok(FinalizeOutcome::Deferred),
804 Err(source) if source.kind() == std::io::ErrorKind::DirectoryNotEmpty => {
805 Ok(FinalizeOutcome::Deferred)
806 }
807 Err(source) => Err(source),
808 };
809 }
810 #[cfg(not(windows))]
811 let _ = running_image;
812 fs::remove_dir_all(backup_root)?;
813 Ok(FinalizeOutcome::Cleaned)
814}
815
816fn revalidate_ownership_locked(
817 txn: &PreparedTransaction,
818 ownership: &CommitOwnership<'_>,
819 preflight: &HashMap<MemberId, Ownership>,
820) -> Result<()> {
821 for member in txn.artifacts().iter() {
822 let destination = txn.destination(member.id())?;
823 let locked = ownership.verifier.verify(member.id(), &destination);
824 let first = preflight
825 .get(member.id())
826 .copied()
827 .unwrap_or(Ownership::Unknown);
828 if locked != first {
831 return Err(Error::DestinationConflict { destination });
832 }
833 match locked {
834 Ownership::Owned => {}
835 Ownership::Absent if ownership.absent == AbsentPolicy::AllowCreate => {}
836 Ownership::Absent | Ownership::Foreign | Ownership::Unknown => {
837 return Err(Error::DestinationConflict { destination });
838 }
839 }
840 require_ready_parent(&destination, txn.plan.installation_root())?;
843 revalidate_destination(&destination, txn.plan.installation_root())?;
844 }
845 txn.plan.revalidate_current_executable()?;
848 Ok(())
849}
850
851fn revalidate_staged_under_lock(
852 txn: &PreparedTransaction,
853 verified_digests: &HashMap<MemberId, [u8; 32]>,
854) -> std::result::Result<(), (Option<MemberId>, Error)> {
855 for member in txn.artifacts().iter() {
856 let expected = verified_digests.get(member.id()).ok_or_else(|| {
857 (
858 Some(member.id().clone()),
859 Error::VerificationFailed(format!(
860 "member {} has no verified digest; commit requires verified integrity",
861 member.id()
862 )),
863 )
864 })?;
865 let staged = txn
866 .staged_path(member.id())
867 .map_err(|e| (Some(member.id().clone()), e))?;
868 let metadata = fs::symlink_metadata(&staged).map_err(|source| {
869 (
870 Some(member.id().clone()),
871 Error::io("reading staged candidate", source),
872 )
873 })?;
874 if !metadata.is_file() {
875 return Err((
876 Some(member.id().clone()),
877 Error::VerificationFailed(format!(
878 "staged member {} is not a regular file",
879 member.id()
880 )),
881 ));
882 }
883 #[cfg(unix)]
884 {
885 use std::os::unix::fs::MetadataExt;
886 if metadata.nlink() != 1 {
887 return Err((
888 Some(member.id().clone()),
889 Error::VerificationFailed(format!(
890 "staged member {} is hard-linked",
891 member.id()
892 )),
893 ));
894 }
895 }
896 let actual = hash_file(&staged).map_err(|e| (Some(member.id().clone()), e))?;
897 if &actual != expected {
898 return Err((
899 Some(member.id().clone()),
900 Error::VerificationFailed(format!(
901 "staged member {} changed after verification",
902 member.id()
903 )),
904 ));
905 }
906 }
907 Ok(())
908}
909
910fn create_backup_directory(root: &Path) -> Result<PathBuf> {
911 use std::time::{SystemTime, UNIX_EPOCH};
912 for _ in 0..32 {
913 let nonce = NEXT_BACKUP_NONCE.fetch_add(1, Ordering::Relaxed);
914 let nanos = SystemTime::now()
915 .duration_since(UNIX_EPOCH)
916 .map(|d| d.as_nanos())
917 .unwrap_or(0);
918 let path = root.join(format!(
919 ".eggup-backup-{}-{nonce}-{nanos}",
920 std::process::id()
921 ));
922 match fs::create_dir(&path) {
923 Ok(()) => {
924 #[cfg(unix)]
925 {
926 use std::os::unix::fs::PermissionsExt;
927 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o700));
928 }
929 return Ok(path);
930 }
931 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
932 Err(source) => return Err(Error::io("creating backup set", source)),
933 }
934 }
935 Err(Error::invalid("could not create a unique backup directory"))
936}
937
938fn revalidate_destination(destination: &Path, root: &Path) -> Result<()> {
939 let root = fs::canonicalize(root)
940 .map_err(|source| Error::io("canonicalizing install root", source))?;
941 let mut ancestor = destination;
942 while !ancestor.exists() {
943 ancestor = ancestor
944 .parent()
945 .ok_or_else(|| Error::invalid("destination has no existing ancestor"))?;
946 }
947 let canonical_ancestor = fs::canonicalize(ancestor)
948 .map_err(|source| Error::io("canonicalizing destination ancestor", source))?;
949 if !canonical_ancestor.starts_with(&root) {
950 return Err(Error::DestinationConflict {
951 destination: destination.to_path_buf(),
952 });
953 }
954 match fs::symlink_metadata(destination) {
955 Ok(metadata) => {
956 if !metadata.is_file() {
957 return Err(Error::DestinationConflict {
958 destination: destination.to_path_buf(),
959 });
960 }
961 #[cfg(unix)]
962 {
963 use std::os::unix::fs::MetadataExt;
964 if metadata.nlink() != 1 {
965 return Err(Error::DestinationConflict {
966 destination: destination.to_path_buf(),
967 });
968 }
969 }
970 }
971 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
972 Err(source) => return Err(Error::io("reading destination during revalidation", source)),
973 }
974 Ok(())
975}
976
977fn require_ready_parent(destination: &Path, root: &Path) -> Result<()> {
984 let parent = destination
985 .parent()
986 .ok_or_else(|| Error::invalid("destination has no parent"))?;
987 let parent_meta = fs::symlink_metadata(parent).map_err(|source| {
988 if source.kind() == std::io::ErrorKind::NotFound {
989 Error::invalid(format!(
990 "destination parent does not exist: {}",
991 parent.display()
992 ))
993 } else {
994 Error::io("reading destination parent", source)
995 }
996 })?;
997 if parent_meta.file_type().is_symlink() {
998 return Err(Error::DestinationConflict {
999 destination: destination.to_path_buf(),
1000 });
1001 }
1002 if !parent_meta.is_dir() {
1003 return Err(Error::DestinationConflict {
1004 destination: destination.to_path_buf(),
1005 });
1006 }
1007 let canonical_root = fs::canonicalize(root)
1008 .map_err(|source| Error::io("canonicalizing install root", source))?;
1009 let canonical_parent = fs::canonicalize(parent)
1010 .map_err(|source| Error::io("canonicalizing destination parent", source))?;
1011 if !canonical_parent.starts_with(&canonical_root) {
1012 return Err(Error::DestinationConflict {
1013 destination: destination.to_path_buf(),
1014 });
1015 }
1016 let mut cursor = Some(parent);
1019 while let Some(p) = cursor {
1020 if p == root {
1021 break;
1022 }
1023 let meta = fs::symlink_metadata(p)
1024 .map_err(|source| Error::io("reading destination ancestor", source))?;
1025 if meta.file_type().is_symlink() {
1026 return Err(Error::DestinationConflict {
1027 destination: destination.to_path_buf(),
1028 });
1029 }
1030 cursor = p.parent();
1031 if let Some(next) = cursor {
1032 if next.as_os_str().is_empty() {
1033 break;
1034 }
1035 } else {
1036 break;
1037 }
1038 }
1039 Ok(())
1040}
1041
1042fn restore_entries(
1043 entries: &[BackupEntry],
1044 committed: &HashSet<MemberId>,
1045 fault: Option<CommitFault>,
1046) -> (bool, Option<FailureReport>) {
1047 let mut verified = true;
1048 let mut rollback_failure: Option<FailureReport> = None;
1049 for entry in entries.iter().rev() {
1050 let rollback_target = match fault.as_ref() {
1051 Some(CommitFault::CommitThenRollback(_, id)) => Some(id),
1052 _ => None,
1053 };
1054 if rollback_target == Some(&entry.member) {
1055 verified = false;
1056 rollback_failure = Some(FailureReport::new(
1057 FailurePhase::Rollback,
1058 Some(entry.member.clone()),
1059 FailureCategory::Injected,
1060 format!("injected rollback failure at {}", entry.member),
1061 ));
1062 continue;
1063 }
1064 if entry.backup.is_some() || committed.contains(&entry.member) {
1065 if let Ok(metadata) = fs::symlink_metadata(&entry.destination) {
1066 if !metadata.is_file() || fs::remove_file(&entry.destination).is_err() {
1067 verified = false;
1068 if rollback_failure.is_none() {
1069 rollback_failure = Some(FailureReport::new(
1070 FailurePhase::Rollback,
1071 Some(entry.member.clone()),
1072 FailureCategory::Filesystem,
1073 format!("could not remove partial member {}", entry.member),
1074 ));
1075 }
1076 continue;
1077 }
1078 }
1079 }
1080 if let Some(backup) = &entry.backup {
1081 if fs::rename(backup, &entry.destination).is_err() {
1082 verified = false;
1083 if rollback_failure.is_none() {
1084 rollback_failure = Some(FailureReport::new(
1085 FailurePhase::Rollback,
1086 Some(entry.member.clone()),
1087 FailureCategory::Filesystem,
1088 format!("could not restore backup for {}", entry.member),
1089 ));
1090 }
1091 }
1092 }
1093 }
1094 for entry in entries {
1095 let restored = match &entry.backup {
1096 Some(_) => fs::symlink_metadata(&entry.destination).is_ok(),
1097 None => {
1098 !committed.contains(&entry.member)
1099 || fs::symlink_metadata(&entry.destination).is_err()
1100 }
1101 };
1102 if !restored {
1103 verified = false;
1104 }
1105 }
1106 (verified, rollback_failure)
1107}
1108
1109fn finish_failure_no_mutation(
1110 transaction: PreparedTransaction,
1111 lock: MutationLock,
1112 nothing_mutated: bool,
1113 report: FailureReport,
1114) -> RecoveryResult<TransactionReceipt> {
1115 debug_assert!(nothing_mutated);
1116 drop(lock);
1117 Ok(TransactionReceipt {
1118 product: transaction.plan.product().clone(),
1119 release: transaction.plan.release().clone(),
1120 disposition: TransactionDisposition::RolledBack,
1121 rollback_performed: false,
1122 rollback_verified: true,
1123 cleanup: CleanupDisposition::Cleaned,
1124 recovery_path: None,
1125 failure: Some(report),
1126 rollback_failure: None,
1127 post_commit_failure: None,
1128 })
1129}
1130
1131fn finish_failure(
1132 transaction: PreparedTransaction,
1133 mut lock: MutationLock,
1134 backup_root: PathBuf,
1135 rollback_verified: bool,
1136 rollback_failure: Option<FailureReport>,
1137 report: FailureReport,
1138) -> RecoveryResult<TransactionReceipt> {
1139 if rollback_verified && rollback_failure.is_none() {
1140 match fs::remove_dir_all(&backup_root) {
1141 Ok(()) => {
1142 return Ok(TransactionReceipt {
1143 product: transaction.plan.product().clone(),
1144 release: transaction.plan.release().clone(),
1145 disposition: TransactionDisposition::RolledBack,
1146 rollback_performed: true,
1147 rollback_verified: true,
1148 cleanup: CleanupDisposition::Cleaned,
1149 recovery_path: None,
1150 failure: Some(report),
1151 rollback_failure: None,
1152 post_commit_failure: None,
1153 });
1154 }
1155 Err(source) => {
1162 let error = Error::io("removing backup set after rollback", source);
1163 return Ok(TransactionReceipt {
1164 product: transaction.plan.product().clone(),
1165 release: transaction.plan.release().clone(),
1166 disposition: TransactionDisposition::RolledBack,
1167 rollback_performed: true,
1168 rollback_verified: true,
1169 cleanup: CleanupDisposition::RetainedForRecovery,
1170 recovery_path: Some(backup_root),
1171 failure: Some(report_for_error(FailurePhase::Finalize, None, &error)),
1172 rollback_failure: None,
1173 post_commit_failure: None,
1174 });
1175 }
1176 }
1177 }
1178 lock.preserve();
1179 Ok(TransactionReceipt {
1180 product: transaction.plan.product().clone(),
1181 release: transaction.plan.release().clone(),
1182 disposition: TransactionDisposition::RecoveryRequired,
1183 rollback_performed: true,
1184 rollback_verified,
1185 cleanup: CleanupDisposition::RetainedForRecovery,
1186 recovery_path: Some(backup_root),
1187 failure: Some(report),
1188 rollback_failure,
1189 post_commit_failure: None,
1190 })
1191}