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)]
19pub enum TransactionDisposition {
20 Committed,
22 RolledBack,
24 RecoveryRequired,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum CleanupDisposition {
35 Cleaned,
37 RetainedForRecovery,
39 DeferredToProcessExit,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum PostCommitFailurePolicy {
53 KeepInstalled,
55 RollBack,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum FailurePhase {
65 Lock,
67 Ownership,
69 StageRevalidation,
71 Backup,
73 Commit,
75 PostCommit,
77 Rollback,
79 Finalize,
81}
82
83impl FailurePhase {
84 pub fn as_str(&self) -> &'static str {
86 match self {
87 Self::Lock => "lock",
88 Self::Ownership => "ownership",
89 Self::StageRevalidation => "stage-revalidation",
90 Self::Backup => "backup",
91 Self::Commit => "commit",
92 Self::PostCommit => "post-commit",
93 Self::Rollback => "rollback",
94 Self::Finalize => "finalize",
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum FailureCategory {
103 LockContention,
105 OwnershipConflict,
107 Verification,
109 Filesystem,
111 InvalidInput,
113 Injected,
115 RetainedEvidence,
117 PostCommitCheck,
119}
120
121impl FailureCategory {
122 pub fn as_str(&self) -> &'static str {
124 match self {
125 Self::LockContention => "lock-contention",
126 Self::OwnershipConflict => "ownership-conflict",
127 Self::Verification => "verification",
128 Self::Filesystem => "filesystem",
129 Self::InvalidInput => "invalid-input",
130 Self::Injected => "injected",
131 Self::RetainedEvidence => "retained-evidence",
132 Self::PostCommitCheck => "post-commit-check",
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct FailureReport {
140 phase: FailurePhase,
141 member: Option<MemberId>,
142 category: FailureCategory,
143 detail: String,
144}
145
146impl FailureReport {
147 pub(crate) fn new(
148 phase: FailurePhase,
149 member: Option<MemberId>,
150 category: FailureCategory,
151 detail: impl Into<String>,
152 ) -> Self {
153 let mut detail = detail.into();
154 if detail.len() > 512 {
156 let boundary = detail
157 .char_indices()
158 .map(|(index, _)| index)
159 .take_while(|index| *index <= 512)
160 .last()
161 .unwrap_or(0);
162 detail.truncate(boundary);
163 }
164 Self {
165 phase,
166 member,
167 category,
168 detail,
169 }
170 }
171
172 pub fn phase(&self) -> FailurePhase {
174 self.phase
175 }
176
177 pub fn member(&self) -> Option<&MemberId> {
179 self.member.as_ref()
180 }
181
182 pub fn category(&self) -> FailureCategory {
184 self.category
185 }
186
187 pub fn detail(&self) -> &str {
189 &self.detail
190 }
191}
192
193pub(crate) fn report_for_error(
194 phase: FailurePhase,
195 member: Option<MemberId>,
196 error: &Error,
197) -> FailureReport {
198 let (category, detail) = match error {
199 Error::UpdateInProgress { lock } => (
200 FailureCategory::LockContention,
201 format!("lock contention at {}", lock.display()),
202 ),
203 Error::DestinationConflict { destination } => (
204 FailureCategory::OwnershipConflict,
205 format!("destination conflict at {}", destination.display()),
206 ),
207 Error::VerificationFailed(m) => (FailureCategory::Verification, m.clone()),
208 Error::CandidateExecution(m) => (FailureCategory::Verification, m.clone()),
209 #[cfg(test)]
210 Error::Injected(m) => (FailureCategory::Injected, m.clone()),
211 Error::InvalidInput(m) => (FailureCategory::InvalidInput, m.clone()),
212 Error::UnknownMember(m) => (FailureCategory::InvalidInput, format!("unknown member {m}")),
213 Error::Io { operation, source } => (
214 FailureCategory::Filesystem,
215 format!("{operation}: {source}"),
216 ),
217 };
218 FailureReport::new(phase, member, category, detail)
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct TransactionReceipt {
224 product: crate::domain::ProductId,
225 release: crate::domain::ReleaseId,
226 disposition: TransactionDisposition,
227 rollback_performed: bool,
228 rollback_verified: bool,
229 cleanup: CleanupDisposition,
230 recovery_path: Option<PathBuf>,
231 failure: Option<FailureReport>,
232 rollback_failure: Option<FailureReport>,
233 post_commit_failure: Option<FailureReport>,
234}
235
236impl TransactionReceipt {
237 pub fn product(&self) -> &crate::domain::ProductId {
239 &self.product
240 }
241
242 pub fn release(&self) -> &crate::domain::ReleaseId {
244 &self.release
245 }
246
247 pub fn disposition(&self) -> TransactionDisposition {
249 self.disposition
250 }
251
252 pub fn rollback_performed(&self) -> bool {
254 self.rollback_performed
255 }
256
257 pub fn rollback_verified(&self) -> bool {
266 self.rollback_verified
267 }
268
269 pub fn cleanup(&self) -> CleanupDisposition {
271 self.cleanup
272 }
273
274 pub fn recovery_path(&self) -> Option<&Path> {
282 self.recovery_path.as_deref()
283 }
284
285 pub fn failure(&self) -> Option<&FailureReport> {
293 self.failure.as_ref()
294 }
295
296 pub fn rollback_failure(&self) -> Option<&FailureReport> {
301 self.rollback_failure.as_ref()
302 }
303
304 pub fn post_commit_failure(&self) -> Option<&FailureReport> {
306 self.post_commit_failure.as_ref()
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
311#[cfg_attr(not(test), allow(dead_code))]
312pub(crate) enum CommitFault {
313 LockCreation,
314 Backup(MemberId),
315 BeforeFirstCommit,
316 Commit(MemberId),
317 CommitThenRollback(MemberId, MemberId),
318 Finalize,
319 PostCommitRollback,
320}
321
322#[derive(Debug)]
323struct BackupEntry {
324 member: MemberId,
325 destination: PathBuf,
326 backup: Option<PathBuf>,
327}
328
329type PostCommitCheck<'a> = (
330 PostCommitFailurePolicy,
331 Box<dyn FnOnce() -> std::result::Result<(), String> + 'a>,
332);
333
334impl PreparedTransaction {
335 pub(crate) fn commit_inner(
336 self,
337 ownership: CommitOwnership<'_>,
338 verified_digests: &HashMap<MemberId, [u8; 32]>,
339 fault: Option<CommitFault>,
340 post_commit: Option<PostCommitCheck<'_>>,
341 stale_recovery: Option<&dyn crate::lock::StaleLockVerifier>,
342 ) -> RecoveryResult<TransactionReceipt> {
343 #[cfg(test)]
344 if fault == Some(CommitFault::LockCreation) {
345 return Err(Error::injected("injected lock creation failure").into());
346 }
347 let preflight = classify_all(&self, &ownership)?;
350 let mut lock = match stale_recovery {
351 None => MutationLock::acquire(
355 self.plan.installation_root(),
356 self.plan.product(),
357 self.plan.release(),
358 )
359 .map_err(RecoveryError::from),
360 Some(verifier) => MutationLock::acquire_with_recovery(
361 self.plan.installation_root(),
362 self.plan.product(),
363 self.plan.release(),
364 verifier,
365 ),
366 }?;
367 if let Err(error) = revalidate_ownership_locked(&self, &ownership, &preflight) {
370 let report = report_for_error(FailurePhase::Ownership, None, &error);
371 let verified = true;
372 return finish_failure_no_mutation(self, lock, verified, report);
373 }
374 if let Err((member, error)) = revalidate_staged_under_lock(&self, verified_digests) {
377 let report = report_for_error(FailurePhase::StageRevalidation, member, &error);
378 return finish_failure_no_mutation(self, lock, true, report);
379 }
380 let backup_root = match create_backup_directory(self.plan.installation_root()) {
381 Ok(p) => p,
382 Err(error) => {
383 let report = report_for_error(FailurePhase::Backup, None, &error);
384 return finish_failure_no_mutation(self, lock, true, report);
385 }
386 };
387 let mut entries = Vec::with_capacity(self.plan.artifacts().len());
388 if let Err((member, error)) = self.backup_members(&mut entries, &backup_root, fault.clone())
389 {
390 let phase = FailurePhase::Backup;
391 let report = report_for_error(phase, member, &error);
392 let (verified, rollback_failure) =
393 restore_entries(&entries, &HashSet::new(), fault.clone());
394 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
395 }
396 #[cfg(test)]
397 if fault == Some(CommitFault::BeforeFirstCommit) {
398 let error = Error::injected("injected pre-commit failure");
399 let report = report_for_error(FailurePhase::Commit, None, &error);
400 let (verified, rollback_failure) =
401 restore_entries(&entries, &HashSet::new(), fault.clone());
402 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
403 }
404
405 let mut committed = HashSet::new();
406 let members = self.plan.artifacts().iter().cloned().collect::<Vec<_>>();
407 for member in members {
408 #[cfg(test)]
409 let commit_should_fail = fault.as_ref().is_some_and(|candidate| match candidate {
410 CommitFault::Commit(id) | CommitFault::CommitThenRollback(id, _) => {
411 id == member.id()
412 }
413 _ => false,
414 });
415 #[cfg(test)]
416 if commit_should_fail {
417 let error = Error::injected(format!("injected commit failure at {}", member.id()));
418 let report =
419 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
420 let (verified, rollback_failure) =
421 restore_entries(&entries, &committed, fault.clone());
422 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
423 }
424 let destination = match self.plan.destination(member.id()) {
425 Ok(destination) => destination,
426 Err(error) => {
427 let report =
428 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
429 let (verified, rollback_failure) =
430 restore_entries(&entries, &committed, fault.clone());
431 return finish_failure(
432 self,
433 lock,
434 backup_root,
435 verified,
436 rollback_failure,
437 report,
438 );
439 }
440 };
441 if let Err(error) = require_ready_parent(&destination, self.plan.installation_root()) {
442 let report =
443 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
444 let (verified, rollback_failure) =
445 restore_entries(&entries, &committed, fault.clone());
446 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
447 }
448 let staged = match self.staged_path(member.id()) {
449 Ok(staged) => staged,
450 Err(error) => {
451 let report =
452 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
453 let (verified, rollback_failure) =
454 restore_entries(&entries, &committed, fault.clone());
455 return finish_failure(
456 self,
457 lock,
458 backup_root,
459 verified,
460 rollback_failure,
461 report,
462 );
463 }
464 };
465 if let Err(source) = fs::rename(&staged, &destination) {
466 let error = Error::io("replacing destination from prepared stage", source);
467 let report =
468 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
469 let (verified, rollback_failure) =
470 restore_entries(&entries, &committed, fault.clone());
471 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
472 }
473 committed.insert(member.id().clone());
474 }
475
476 let mut post_commit_failure = None;
477 if let Some((policy, check)) = post_commit {
478 let checked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(check));
479 let failure_detail = match checked {
480 Ok(Ok(())) => None,
481 Ok(Err(detail)) => Some(detail),
482 Err(_) => Some("post-commit check panicked".to_string()),
483 };
484 if let Some(detail) = failure_detail {
485 let report = FailureReport::new(
486 FailurePhase::PostCommit,
487 None,
488 FailureCategory::PostCommitCheck,
489 detail,
490 );
491 match policy {
492 PostCommitFailurePolicy::KeepInstalled => {
493 post_commit_failure = Some(report);
494 }
495 PostCommitFailurePolicy::RollBack => {
496 let rollback_fault = if fault == Some(CommitFault::PostCommitRollback) {
497 Some(CommitFault::CommitThenRollback(
498 self.plan
499 .artifacts()
500 .iter()
501 .next()
502 .expect("non-empty artifact set")
503 .id()
504 .clone(),
505 self.plan
506 .artifacts()
507 .iter()
508 .next()
509 .expect("non-empty artifact set")
510 .id()
511 .clone(),
512 ))
513 } else {
514 fault.clone()
515 };
516 let (verified, rollback_failure) =
517 restore_entries(&entries, &committed, rollback_fault);
518 let mut receipt = finish_failure(
519 self,
520 lock,
521 backup_root,
522 verified,
523 rollback_failure,
524 report.clone(),
525 )?;
526 receipt.post_commit_failure = Some(report);
527 return Ok(receipt);
528 }
529 }
530 }
531 }
532
533 if fault == Some(CommitFault::Finalize) {
534 lock.preserve();
535 let report = FailureReport::new(
536 FailurePhase::Finalize,
537 None,
538 FailureCategory::Injected,
539 "injected finalize failure",
540 );
541 return Ok(TransactionReceipt {
542 product: self.plan.product().clone(),
543 release: self.plan.release().clone(),
544 disposition: TransactionDisposition::Committed,
545 rollback_performed: false,
546 rollback_verified: false,
547 cleanup: CleanupDisposition::RetainedForRecovery,
548 recovery_path: Some(backup_root),
549 failure: Some(report),
550 rollback_failure: None,
551 post_commit_failure,
552 });
553 }
554 match finalize_backup_set(&self.plan, &backup_root, &entries) {
560 Ok(FinalizeOutcome::Cleaned) => Ok(TransactionReceipt {
561 product: self.plan.product().clone(),
562 release: self.plan.release().clone(),
563 disposition: TransactionDisposition::Committed,
564 rollback_performed: false,
565 rollback_verified: false,
566 cleanup: CleanupDisposition::Cleaned,
567 recovery_path: None,
568 failure: None,
569 rollback_failure: None,
570 post_commit_failure,
571 }),
572 Ok(FinalizeOutcome::Deferred) => Ok(TransactionReceipt {
573 product: self.plan.product().clone(),
574 release: self.plan.release().clone(),
575 disposition: TransactionDisposition::Committed,
576 rollback_performed: false,
577 rollback_verified: false,
578 cleanup: CleanupDisposition::DeferredToProcessExit,
579 recovery_path: None,
582 failure: None,
583 rollback_failure: None,
584 post_commit_failure,
585 }),
586 Err(source) => {
587 lock.preserve();
588 let error = Error::io("removing backup set after commit", source);
589 let report = report_for_error(FailurePhase::Finalize, None, &error);
590 Ok(TransactionReceipt {
591 product: self.plan.product().clone(),
592 release: self.plan.release().clone(),
593 disposition: TransactionDisposition::Committed,
594 rollback_performed: false,
595 rollback_verified: false,
596 cleanup: CleanupDisposition::RetainedForRecovery,
597 recovery_path: Some(backup_root),
598 failure: Some(report),
599 rollback_failure: None,
600 post_commit_failure,
601 })
602 }
603 }
604 }
605
606 fn backup_members(
607 &self,
608 entries: &mut Vec<BackupEntry>,
609 backup_root: &Path,
610 fault: Option<CommitFault>,
611 ) -> std::result::Result<(), (Option<MemberId>, Error)> {
612 #[cfg(not(test))]
613 let _ = &fault;
614 let members = self.plan.artifacts().iter().cloned().collect::<Vec<_>>();
615 for member in members {
616 #[cfg(test)]
617 if fault == Some(CommitFault::Backup(member.id().clone())) {
618 return Err((
619 Some(member.id().clone()),
620 Error::injected(format!("injected backup failure at {}", member.id())),
621 ));
622 }
623 let destination = self
624 .plan
625 .destination(member.id())
626 .map_err(|e| (Some(member.id().clone()), e))?;
627 revalidate_destination(&destination, self.plan.installation_root())
628 .map_err(|e| (Some(member.id().clone()), e))?;
629 self.plan
633 .revalidate_current_executable()
634 .map_err(|e| (Some(member.id().clone()), e))?;
635 let backup = match fs::symlink_metadata(&destination) {
636 Ok(_) => {
637 let path = backup_root.join(member.destination());
638 if let Some(parent) = path.parent() {
639 fs::create_dir_all(parent).map_err(|source| {
640 (
641 Some(member.id().clone()),
642 Error::io("creating backup directory", source),
643 )
644 })?;
645 }
646 fs::rename(&destination, &path).map_err(|source| {
647 (
648 Some(member.id().clone()),
649 Error::io("backing up existing destination", source),
650 )
651 })?;
652 Some(path)
653 }
654 Err(source) if source.kind() == std::io::ErrorKind::NotFound => None,
655 Err(source) => {
656 return Err((
657 Some(member.id().clone()),
658 Error::io("reading destination before backup", source),
659 ));
660 }
661 };
662 entries.push(BackupEntry {
663 member: member.id().clone(),
664 destination,
665 backup,
666 });
667 }
668 Ok(())
669 }
670}
671
672fn classify_all(
673 txn: &PreparedTransaction,
674 ownership: &CommitOwnership<'_>,
675) -> Result<HashMap<MemberId, Ownership>> {
676 let mut out = HashMap::new();
677 for member in txn.artifacts().iter() {
678 let destination = txn.destination(member.id())?;
679 out.insert(
680 member.id().clone(),
681 ownership.verifier.verify(member.id(), &destination),
682 );
683 }
684 Ok(out)
685}
686
687#[cfg_attr(not(windows), allow(dead_code))]
689enum FinalizeOutcome {
690 Cleaned,
692 Deferred,
694}
695
696fn finalize_backup_set(
706 plan: &InstallPlan,
707 backup_root: &Path,
708 entries: &[BackupEntry],
709) -> std::io::Result<FinalizeOutcome> {
710 let running_image = plan
713 .current_executable()
714 .and_then(|_| entries.iter().find_map(|entry| entry.backup.clone()));
715 #[cfg(windows)]
716 if let Some(backup) = running_image {
717 self_replace::self_delete_at(&backup).map_err(|source| {
721 std::io::Error::other(format!(
722 "scheduling removal of the replaced running image: {source}"
723 ))
724 })?;
725 return match fs::remove_dir(backup_root) {
728 Ok(()) => Ok(FinalizeOutcome::Deferred),
729 Err(source) if source.kind() == std::io::ErrorKind::DirectoryNotEmpty => {
730 Ok(FinalizeOutcome::Deferred)
731 }
732 Err(source) => Err(source),
733 };
734 }
735 #[cfg(not(windows))]
736 let _ = running_image;
737 fs::remove_dir_all(backup_root)?;
738 Ok(FinalizeOutcome::Cleaned)
739}
740
741fn revalidate_ownership_locked(
742 txn: &PreparedTransaction,
743 ownership: &CommitOwnership<'_>,
744 preflight: &HashMap<MemberId, Ownership>,
745) -> Result<()> {
746 for member in txn.artifacts().iter() {
747 let destination = txn.destination(member.id())?;
748 let locked = ownership.verifier.verify(member.id(), &destination);
749 let first = preflight
750 .get(member.id())
751 .copied()
752 .unwrap_or(Ownership::Unknown);
753 if locked != first {
756 return Err(Error::DestinationConflict { destination });
757 }
758 match locked {
759 Ownership::Owned => {}
760 Ownership::Absent if ownership.absent == AbsentPolicy::AllowCreate => {}
761 Ownership::Absent | Ownership::Foreign | Ownership::Unknown => {
762 return Err(Error::DestinationConflict { destination });
763 }
764 }
765 require_ready_parent(&destination, txn.plan.installation_root())?;
768 revalidate_destination(&destination, txn.plan.installation_root())?;
769 }
770 txn.plan.revalidate_current_executable()?;
773 Ok(())
774}
775
776fn revalidate_staged_under_lock(
777 txn: &PreparedTransaction,
778 verified_digests: &HashMap<MemberId, [u8; 32]>,
779) -> std::result::Result<(), (Option<MemberId>, Error)> {
780 for member in txn.artifacts().iter() {
781 let expected = verified_digests.get(member.id()).ok_or_else(|| {
782 (
783 Some(member.id().clone()),
784 Error::VerificationFailed(format!(
785 "member {} has no verified digest; commit requires verified integrity",
786 member.id()
787 )),
788 )
789 })?;
790 let staged = txn
791 .staged_path(member.id())
792 .map_err(|e| (Some(member.id().clone()), e))?;
793 let metadata = fs::symlink_metadata(&staged).map_err(|source| {
794 (
795 Some(member.id().clone()),
796 Error::io("reading staged candidate", source),
797 )
798 })?;
799 if !metadata.is_file() {
800 return Err((
801 Some(member.id().clone()),
802 Error::VerificationFailed(format!(
803 "staged member {} is not a regular file",
804 member.id()
805 )),
806 ));
807 }
808 #[cfg(unix)]
809 {
810 use std::os::unix::fs::MetadataExt;
811 if metadata.nlink() != 1 {
812 return Err((
813 Some(member.id().clone()),
814 Error::VerificationFailed(format!(
815 "staged member {} is hard-linked",
816 member.id()
817 )),
818 ));
819 }
820 }
821 let actual = hash_file(&staged).map_err(|e| (Some(member.id().clone()), e))?;
822 if &actual != expected {
823 return Err((
824 Some(member.id().clone()),
825 Error::VerificationFailed(format!(
826 "staged member {} changed after verification",
827 member.id()
828 )),
829 ));
830 }
831 }
832 Ok(())
833}
834
835fn create_backup_directory(root: &Path) -> Result<PathBuf> {
836 use std::time::{SystemTime, UNIX_EPOCH};
837 for _ in 0..32 {
838 let nonce = NEXT_BACKUP_NONCE.fetch_add(1, Ordering::Relaxed);
839 let nanos = SystemTime::now()
840 .duration_since(UNIX_EPOCH)
841 .map(|d| d.as_nanos())
842 .unwrap_or(0);
843 let path = root.join(format!(
844 ".eggup-backup-{}-{nonce}-{nanos}",
845 std::process::id()
846 ));
847 match fs::create_dir(&path) {
848 Ok(()) => {
849 #[cfg(unix)]
850 {
851 use std::os::unix::fs::PermissionsExt;
852 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o700));
853 }
854 return Ok(path);
855 }
856 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
857 Err(source) => return Err(Error::io("creating backup set", source)),
858 }
859 }
860 Err(Error::invalid("could not create a unique backup directory"))
861}
862
863fn revalidate_destination(destination: &Path, root: &Path) -> Result<()> {
864 let root = fs::canonicalize(root)
865 .map_err(|source| Error::io("canonicalizing install root", source))?;
866 let mut ancestor = destination;
867 while !ancestor.exists() {
868 ancestor = ancestor
869 .parent()
870 .ok_or_else(|| Error::invalid("destination has no existing ancestor"))?;
871 }
872 let canonical_ancestor = fs::canonicalize(ancestor)
873 .map_err(|source| Error::io("canonicalizing destination ancestor", source))?;
874 if !canonical_ancestor.starts_with(&root) {
875 return Err(Error::DestinationConflict {
876 destination: destination.to_path_buf(),
877 });
878 }
879 match fs::symlink_metadata(destination) {
880 Ok(metadata) => {
881 if !metadata.is_file() {
882 return Err(Error::DestinationConflict {
883 destination: destination.to_path_buf(),
884 });
885 }
886 #[cfg(unix)]
887 {
888 use std::os::unix::fs::MetadataExt;
889 if metadata.nlink() != 1 {
890 return Err(Error::DestinationConflict {
891 destination: destination.to_path_buf(),
892 });
893 }
894 }
895 }
896 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
897 Err(source) => return Err(Error::io("reading destination during revalidation", source)),
898 }
899 Ok(())
900}
901
902fn require_ready_parent(destination: &Path, root: &Path) -> Result<()> {
909 let parent = destination
910 .parent()
911 .ok_or_else(|| Error::invalid("destination has no parent"))?;
912 let parent_meta = fs::symlink_metadata(parent).map_err(|source| {
913 if source.kind() == std::io::ErrorKind::NotFound {
914 Error::invalid(format!(
915 "destination parent does not exist: {}",
916 parent.display()
917 ))
918 } else {
919 Error::io("reading destination parent", source)
920 }
921 })?;
922 if parent_meta.file_type().is_symlink() {
923 return Err(Error::DestinationConflict {
924 destination: destination.to_path_buf(),
925 });
926 }
927 if !parent_meta.is_dir() {
928 return Err(Error::DestinationConflict {
929 destination: destination.to_path_buf(),
930 });
931 }
932 let canonical_root = fs::canonicalize(root)
933 .map_err(|source| Error::io("canonicalizing install root", source))?;
934 let canonical_parent = fs::canonicalize(parent)
935 .map_err(|source| Error::io("canonicalizing destination parent", source))?;
936 if !canonical_parent.starts_with(&canonical_root) {
937 return Err(Error::DestinationConflict {
938 destination: destination.to_path_buf(),
939 });
940 }
941 let mut cursor = Some(parent);
944 while let Some(p) = cursor {
945 if p == root {
946 break;
947 }
948 let meta = fs::symlink_metadata(p)
949 .map_err(|source| Error::io("reading destination ancestor", source))?;
950 if meta.file_type().is_symlink() {
951 return Err(Error::DestinationConflict {
952 destination: destination.to_path_buf(),
953 });
954 }
955 cursor = p.parent();
956 if let Some(next) = cursor {
957 if next.as_os_str().is_empty() {
958 break;
959 }
960 } else {
961 break;
962 }
963 }
964 Ok(())
965}
966
967fn restore_entries(
968 entries: &[BackupEntry],
969 committed: &HashSet<MemberId>,
970 fault: Option<CommitFault>,
971) -> (bool, Option<FailureReport>) {
972 let mut verified = true;
973 let mut rollback_failure: Option<FailureReport> = None;
974 for entry in entries.iter().rev() {
975 let rollback_target = match fault.as_ref() {
976 Some(CommitFault::CommitThenRollback(_, id)) => Some(id),
977 _ => None,
978 };
979 if rollback_target == Some(&entry.member) {
980 verified = false;
981 rollback_failure = Some(FailureReport::new(
982 FailurePhase::Rollback,
983 Some(entry.member.clone()),
984 FailureCategory::Injected,
985 format!("injected rollback failure at {}", entry.member),
986 ));
987 continue;
988 }
989 if entry.backup.is_some() || committed.contains(&entry.member) {
990 if let Ok(metadata) = fs::symlink_metadata(&entry.destination) {
991 if !metadata.is_file() || fs::remove_file(&entry.destination).is_err() {
992 verified = false;
993 if rollback_failure.is_none() {
994 rollback_failure = Some(FailureReport::new(
995 FailurePhase::Rollback,
996 Some(entry.member.clone()),
997 FailureCategory::Filesystem,
998 format!("could not remove partial member {}", entry.member),
999 ));
1000 }
1001 continue;
1002 }
1003 }
1004 }
1005 if let Some(backup) = &entry.backup {
1006 if fs::rename(backup, &entry.destination).is_err() {
1007 verified = false;
1008 if rollback_failure.is_none() {
1009 rollback_failure = Some(FailureReport::new(
1010 FailurePhase::Rollback,
1011 Some(entry.member.clone()),
1012 FailureCategory::Filesystem,
1013 format!("could not restore backup for {}", entry.member),
1014 ));
1015 }
1016 }
1017 }
1018 }
1019 for entry in entries {
1020 let restored = match &entry.backup {
1021 Some(_) => fs::symlink_metadata(&entry.destination).is_ok(),
1022 None => {
1023 !committed.contains(&entry.member)
1024 || fs::symlink_metadata(&entry.destination).is_err()
1025 }
1026 };
1027 if !restored {
1028 verified = false;
1029 }
1030 }
1031 (verified, rollback_failure)
1032}
1033
1034fn finish_failure_no_mutation(
1035 transaction: PreparedTransaction,
1036 lock: MutationLock,
1037 nothing_mutated: bool,
1038 report: FailureReport,
1039) -> RecoveryResult<TransactionReceipt> {
1040 debug_assert!(nothing_mutated);
1041 drop(lock);
1042 Ok(TransactionReceipt {
1043 product: transaction.plan.product().clone(),
1044 release: transaction.plan.release().clone(),
1045 disposition: TransactionDisposition::RolledBack,
1046 rollback_performed: false,
1047 rollback_verified: true,
1048 cleanup: CleanupDisposition::Cleaned,
1049 recovery_path: None,
1050 failure: Some(report),
1051 rollback_failure: None,
1052 post_commit_failure: None,
1053 })
1054}
1055
1056fn finish_failure(
1057 transaction: PreparedTransaction,
1058 mut lock: MutationLock,
1059 backup_root: PathBuf,
1060 rollback_verified: bool,
1061 rollback_failure: Option<FailureReport>,
1062 report: FailureReport,
1063) -> RecoveryResult<TransactionReceipt> {
1064 if rollback_verified && rollback_failure.is_none() {
1065 match fs::remove_dir_all(&backup_root) {
1066 Ok(()) => {
1067 return Ok(TransactionReceipt {
1068 product: transaction.plan.product().clone(),
1069 release: transaction.plan.release().clone(),
1070 disposition: TransactionDisposition::RolledBack,
1071 rollback_performed: true,
1072 rollback_verified: true,
1073 cleanup: CleanupDisposition::Cleaned,
1074 recovery_path: None,
1075 failure: Some(report),
1076 rollback_failure: None,
1077 post_commit_failure: None,
1078 });
1079 }
1080 Err(source) => {
1087 let error = Error::io("removing backup set after rollback", source);
1088 return Ok(TransactionReceipt {
1089 product: transaction.plan.product().clone(),
1090 release: transaction.plan.release().clone(),
1091 disposition: TransactionDisposition::RolledBack,
1092 rollback_performed: true,
1093 rollback_verified: true,
1094 cleanup: CleanupDisposition::RetainedForRecovery,
1095 recovery_path: Some(backup_root),
1096 failure: Some(report_for_error(FailurePhase::Finalize, None, &error)),
1097 rollback_failure: None,
1098 post_commit_failure: None,
1099 });
1100 }
1101 }
1102 }
1103 lock.preserve();
1104 Ok(TransactionReceipt {
1105 product: transaction.plan.product().clone(),
1106 release: transaction.plan.release().clone(),
1107 disposition: TransactionDisposition::RecoveryRequired,
1108 rollback_performed: true,
1109 rollback_verified,
1110 cleanup: CleanupDisposition::RetainedForRecovery,
1111 recovery_path: Some(backup_root),
1112 failure: Some(report),
1113 rollback_failure,
1114 post_commit_failure: None,
1115 })
1116}