#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![doc = "Policy-neutral local mechanics for verified multi-artifact updates."]
#![doc = ""]
#![doc = "The core crate deliberately does not fetch bytes, manage services, or choose release policy."]
#![doc = "Callers acquire artifacts and prove destination ownership; the core"]
#![doc = "provides validated preparation, SHA-256 integrity verification, bounded"]
#![doc = "candidate validation, locked ownership revalidation, staged-digest"]
#![doc = "revalidation, and atomic-feeling multi-artifact commit with rollback."]
#![doc = ""]
#![doc = "Integrity here is checksum evidence only. No authenticity or signature"]
#![doc = "claim is made: there is no authenticity verifier in this crate."]
#![doc = ""]
#![doc = "Runnable flows live in `examples/`: one-member, multi-member, custom"]
#![doc = "validator, ownership verifier, and receipt interpretation."]
mod candidate;
mod current_exe;
mod domain;
mod error;
mod integrity;
mod lock;
mod stage;
mod transaction;
pub use candidate::{
run_bounded, AllValidators, CandidateValidator, CommandOutput, CommandSpec,
CrossMemberAgreementValidator, ExactIdentityValidator, ValidatedTransaction,
};
pub use current_exe::CurrentExecutable;
pub use domain::{
AbsentOnlyVerifier, AbsentPolicy, ArtifactMember, ArtifactSet, BoundSources, CommitOwnership,
ExactDigestVerifier, ExistingAsOwnedVerifier, FileKind, InstallPlan, IntegrityRequirement,
MemberId, Ownership, OwnershipVerifier, PermissionsIntent, ProductId, ReleaseId,
StagePlacement,
};
pub use error::{Error, RecoveryError, RecoveryResult, Result};
pub use integrity::{
hash_file, parse_sha256_sidecar, verify_file, IntegrityResult, IntegrityStatus, Sha256Manifest,
VerifiedTransaction,
};
pub use lock::{LockObservation, LockStatus, MutationLock, StaleLockDecision, StaleLockVerifier};
pub use stage::PreparedTransaction;
pub use transaction::{
CleanupDisposition, FailureCategory, FailurePhase, FailureReport, PostCommitFailurePolicy,
TransactionDisposition, TransactionReceipt,
};
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests {
use super::test_support::{FailureInjector, FailurePoint, InstallationRoot};
use super::transaction::CommitFault;
use super::{
AbsentOnlyVerifier, AbsentPolicy, AllValidators, ArtifactMember, ArtifactSet, BoundSources,
CleanupDisposition, CommitOwnership, Error, ExactDigestVerifier, ExactIdentityValidator,
ExistingAsOwnedVerifier, InstallPlan, MemberId, MutationLock, Ownership, OwnershipVerifier,
PostCommitFailurePolicy, ProductId, ReleaseId, TransactionDisposition,
};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[cfg(unix)]
use std::time::Duration;
struct FixedVerifier(Ownership);
impl std::fmt::Debug for FixedVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("FixedVerifier").field(&self.0).finish()
}
}
impl OwnershipVerifier for FixedVerifier {
fn verify(&self, _m: &MemberId, _d: &Path) -> Ownership {
self.0
}
}
fn allow_create() -> ExistingAsOwnedVerifier {
ExistingAsOwnedVerifier
}
fn commit_verified(
prepared: super::PreparedTransaction,
verifier: &dyn OwnershipVerifier,
absent: AbsentPolicy,
) -> super::Result<super::TransactionReceipt> {
let verified = prepared.verify_integrity()?;
let validated = verified.validate(&AllValidators::new())?;
validated.commit(CommitOwnership::new(verifier, absent))
}
fn commit_verified_with_fault(
prepared: super::PreparedTransaction,
verifier: &dyn OwnershipVerifier,
absent: AbsentPolicy,
fault: CommitFault,
) -> super::Result<super::TransactionReceipt> {
let verified = prepared.verify_integrity()?;
let validated = verified.validate(&AllValidators::new())?;
validated.commit_with_fault(CommitOwnership::new(verifier, absent), fault)
}
#[test]
fn fixture_writes_and_reads_exact_bytes() {
let fixture = InstallationRoot::new().expect("fixture");
fixture
.write_file("bin/eggup", b"exact bytes")
.expect("write");
assert_eq!(
fixture.read_file("bin/eggup").expect("read"),
b"exact bytes"
);
assert!(fixture.path().join("bin/eggup").is_file());
}
#[test]
fn fixture_rejects_escape_paths() {
let fixture = InstallationRoot::new().expect("fixture");
let error = fixture.write_file("../outside", b"nope").unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
assert!(!fixture.path().parent().unwrap().join("outside").exists());
}
#[test]
fn failure_injector_defaults_to_no_failure() {
let mut injector = FailureInjector::default();
assert!(injector.check(FailurePoint::Prepare).is_ok());
assert!(injector.check(FailurePoint::Commit).is_ok());
}
#[test]
fn prepares_a_multi_member_bundle_without_touching_destinations() {
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
inputs.write_file("egress", b"egress").expect("write");
inputs.write_file("helper", b"helper").expect("write");
let product = ProductId::new("egress").expect("product");
let release = ReleaseId::new("2026.09.22").expect("release");
let members = ArtifactSet::new(vec![
ArtifactMember::new(
MemberId::new("main").expect("member"),
inputs.path().join("egress"),
"bin/egress",
)
.expect("artifact"),
ArtifactMember::new(
MemberId::new("helper").expect("member"),
inputs.path().join("helper"),
"bin/helper",
)
.expect("artifact"),
])
.expect("members");
let prepared = InstallPlan::new(product, release, install.path(), members)
.expect("plan")
.prepare()
.expect("prepare");
assert!(!install.path().join("bin/egress").exists());
assert_eq!(
fs::read(
prepared
.staged_path(&MemberId::new("main").unwrap())
.unwrap()
)
.unwrap(),
b"egress"
);
}
#[test]
fn preparation_failure_cleans_private_stage() {
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
inputs.write_file("eggup", b"bytes").expect("write");
let plan = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(
ArtifactMember::new(
MemberId::new("main").unwrap(),
inputs.path().join("eggup"),
"eggup",
)
.unwrap(),
)
.unwrap(),
)
.unwrap();
assert!(plan
.clone()
.prepare_with_failure(FailurePoint::StageCreate)
.is_err());
let result = plan.prepare_with_failure(FailurePoint::StageCopy);
assert!(result.is_err());
assert!(!install.path().join("eggup").exists());
let stage_prefix = format!(
".eggup-stage-{}-",
install.path().file_name().unwrap().to_string_lossy()
);
assert!(install
.path()
.parent()
.unwrap()
.read_dir()
.unwrap()
.filter_map(std::result::Result::ok)
.all(|entry| !entry
.file_name()
.to_string_lossy()
.starts_with(&stage_prefix)));
}
#[test]
fn rejects_duplicate_normalized_destinations() {
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
inputs.write_file("one", b"one").expect("write");
inputs.write_file("two", b"two").expect("write");
let members = ArtifactSet::new(vec![
ArtifactMember::new(
MemberId::new("one").unwrap(),
inputs.path().join("one"),
"bin/./app",
)
.unwrap(),
ArtifactMember::new(
MemberId::new("two").unwrap(),
inputs.path().join("two"),
"bin/app",
)
.unwrap(),
])
.unwrap();
let result = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
members,
);
assert!(result.is_err());
}
#[test]
fn rejects_empty_sets_and_escaping_destinations() {
assert!(ArtifactSet::new(Vec::new()).is_err());
assert!(ArtifactMember::new(
MemberId::new("main").unwrap(),
"/tmp/acquired",
"../outside",
)
.is_err());
assert!(ArtifactMember::new(
MemberId::new("main").unwrap(),
"/tmp/acquired",
"/absolute/path",
)
.is_err());
}
#[test]
fn bound_source_stages_open_object_despite_foreign_replacement() {
use std::fs::File;
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let source = inputs.write_file("owned", b"owned-bytes").expect("write");
let handle = File::open(&source).expect("open");
let member =
ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
.unwrap();
let plan = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap();
fs::remove_file(&source).expect("remove");
fs::write(&source, b"foreign-bytes").expect("foreign");
let mut bound = BoundSources::new();
bound.insert(MemberId::new("main").unwrap(), handle);
let prepared = plan.prepare_with_bound_sources(bound).unwrap();
assert_eq!(
fs::read(
prepared
.staged_path(&MemberId::new("main").unwrap())
.unwrap()
)
.unwrap(),
b"owned-bytes"
);
assert_eq!(fs::read(&source).unwrap(), b"foreign-bytes");
}
#[test]
fn bound_source_stages_open_object_after_root_rename() {
use std::fs::File;
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let subdir = inputs.path().join("owned-dir");
fs::create_dir(&subdir).expect("mkdir");
let source = subdir.join("member");
fs::write(&source, b"owned-bytes").expect("write");
let handle = File::open(&source).expect("open");
let member =
ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
.unwrap();
let plan = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap();
#[cfg(unix)]
fs::rename(&subdir, inputs.path().join("renamed-dir")).expect("rename");
#[cfg(windows)]
fs::rename(&source, subdir.join("renamed-member")).expect("rename");
let mut bound = BoundSources::new();
bound.insert(MemberId::new("main").unwrap(), handle);
let prepared = plan.prepare_with_bound_sources(bound).unwrap();
assert_eq!(
fs::read(
prepared
.staged_path(&MemberId::new("main").unwrap())
.unwrap()
)
.unwrap(),
b"owned-bytes"
);
}
#[test]
fn bound_source_stages_full_bytes_from_nonzero_cursor() {
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let source = inputs.write_file("owned", b"owned-bytes").expect("write");
let mut handle = File::open(&source).expect("open");
let mut prefix = [0u8; 5];
handle.read_exact(&mut prefix).expect("read");
assert_eq!(&prefix, b"owned");
handle.seek(SeekFrom::Start(2)).expect("seek");
let member =
ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
.unwrap();
let plan = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap();
let mut bound = BoundSources::new();
bound.insert(MemberId::new("main").unwrap(), handle);
let prepared = plan.prepare_with_bound_sources(bound).unwrap();
assert_eq!(
fs::read(
prepared
.staged_path(&MemberId::new("main").unwrap())
.unwrap()
)
.unwrap(),
b"owned-bytes"
);
}
#[test]
fn bound_source_for_unknown_member_fails_closed() {
use std::fs::File;
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let source = inputs.write_file("owned", b"owned-bytes").expect("write");
let handle = File::open(&source).expect("open");
let member =
ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
.unwrap();
let plan = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap();
let mut bound = BoundSources::new();
bound.insert(MemberId::new("ghost").unwrap(), handle);
assert!(plan.prepare_with_bound_sources(bound).is_err());
assert!(!install.path().join("bin/main").exists());
}
#[cfg(unix)]
#[test]
fn rejects_symlink_sources_and_preserves_executable_intent_in_stage() {
use std::os::unix::fs::symlink;
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
inputs.write_file("real", b"real").expect("write");
symlink(inputs.path().join("real"), inputs.path().join("link")).expect("symlink");
let symlink_member = ArtifactMember::new(
MemberId::new("link").unwrap(),
inputs.path().join("link"),
"bin/link",
)
.unwrap();
assert!(InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(symlink_member).unwrap(),
)
.is_err());
let member = ArtifactMember::new(
MemberId::new("real").unwrap(),
inputs.path().join("real"),
"bin/real",
)
.unwrap()
.with_permissions(super::PermissionsIntent::Executable);
let prepared = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap()
.prepare()
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = fs::metadata(
prepared
.staged_path(&MemberId::new("real").unwrap())
.unwrap(),
)
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
assert_eq!(mode & 0o777, 0o700);
}
}
#[cfg(unix)]
#[test]
fn bound_source_preserves_executable_intent_that_the_fresh_stage_masks() {
use std::fs::File;
use std::os::unix::fs::PermissionsExt;
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let source = inputs.write_file("owned", b"owned-bytes").expect("write");
fs::set_permissions(&source, fs::Permissions::from_mode(0o755)).expect("chmod source");
let handle = File::open(&source).expect("open");
let digest = super::hash_file(&source).unwrap();
fs::create_dir_all(install.path().join("bin")).expect("bin");
let member =
ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(digest));
let plan = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap();
let mut bound = BoundSources::new();
bound.insert(MemberId::new("main").unwrap(), handle);
let prepared = plan.prepare_with_bound_sources(bound).unwrap();
let staged = prepared
.staged_path(&MemberId::new("main").unwrap())
.unwrap();
let staged_mode = fs::metadata(&staged).unwrap().permissions().mode();
assert_ne!(
staged_mode & 0o111,
0,
"bound staging must not read the hardcoded 0600 back as source intent"
);
assert_eq!(staged_mode & 0o777, 0o700);
let receipt =
commit_verified(prepared, &allow_create(), AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
let live_mode = fs::metadata(install.path().join("bin/main"))
.unwrap()
.permissions()
.mode();
assert_ne!(
live_mode & 0o111,
0,
"installed executable must land executable"
);
assert_eq!(live_mode & 0o777, 0o700);
}
#[cfg(unix)]
#[test]
fn bound_source_does_not_widen_a_non_executable_source() {
use std::fs::File;
use std::os::unix::fs::PermissionsExt;
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let source = inputs.write_file("owned", b"owned-bytes").expect("write");
fs::set_permissions(&source, fs::Permissions::from_mode(0o644)).expect("chmod source");
let handle = File::open(&source).expect("open");
let member =
ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
.unwrap();
let plan = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap();
let mut bound = BoundSources::new();
bound.insert(MemberId::new("main").unwrap(), handle);
let prepared = plan.prepare_with_bound_sources(bound).unwrap();
let staged_mode = fs::metadata(
prepared
.staged_path(&MemberId::new("main").unwrap())
.unwrap(),
)
.unwrap()
.permissions()
.mode();
assert_eq!(staged_mode & 0o777, 0o600);
}
#[test]
fn fault_category_is_structural_not_message_substring() {
let report = super::transaction::report_for_error(
super::FailurePhase::Ownership,
None,
&Error::invalid("member name 'injected' is reserved"),
);
assert_eq!(report.category(), super::FailureCategory::InvalidInput);
let injected = super::transaction::report_for_error(
super::FailurePhase::Ownership,
None,
&Error::injected("injected preparation failure at Copy"),
);
assert_eq!(injected.category(), super::FailureCategory::Injected);
}
fn prepared_bundle(
existing: bool,
) -> (
InstallationRoot,
InstallationRoot,
super::PreparedTransaction,
) {
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let main_src = inputs.write_file("main", b"new-main").expect("write");
let helper_src = inputs.write_file("helper", b"new-helper").expect("write");
let main_digest = super::hash_file(&main_src).unwrap();
let helper_digest = super::hash_file(&helper_src).unwrap();
if existing {
install
.write_file("bin/main", b"old-main")
.expect("old main");
install
.write_file("bin/helper", b"old-helper")
.expect("old helper");
} else {
fs::create_dir_all(install.path().join("bin")).unwrap();
}
let members = ArtifactSet::new(vec![
ArtifactMember::new(
MemberId::new("main").unwrap(),
inputs.path().join("main"),
"bin/main",
)
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
ArtifactMember::new(
MemberId::new("helper").unwrap(),
inputs.path().join("helper"),
"bin/helper",
)
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
])
.unwrap();
let prepared = InstallPlan::new(
ProductId::new("bundle").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
members,
)
.unwrap()
.prepare()
.unwrap();
(inputs, install, prepared)
}
#[test]
fn commits_a_complete_multi_member_generation_and_releases_lock() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let receipt =
commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).expect("commit");
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
assert_eq!(receipt.cleanup(), CleanupDisposition::Cleaned);
assert!(receipt.failure().is_none());
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"new-main"
);
assert_eq!(
fs::read(install.path().join("bin/helper")).unwrap(),
b"new-helper"
);
assert!(!install.path().join(".eggup-mutation.lock").exists());
assert!(install
.path()
.read_dir()
.unwrap()
.filter_map(std::result::Result::ok)
.all(|entry| !entry
.file_name()
.to_string_lossy()
.starts_with(".eggup-backup-")));
}
#[test]
fn post_commit_check_sees_complete_generation_and_holds_mutation_lock() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let validated = prepared
.verify_integrity()
.unwrap()
.validate(&AllValidators::new())
.unwrap();
let receipt = validated
.commit_with_post_commit(
CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
PostCommitFailurePolicy::RollBack,
|| {
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"new-main"
);
assert_eq!(
fs::read(install.path().join("bin/helper")).unwrap(),
b"new-helper"
);
assert!(MutationLock::acquire(
install.path(),
&ProductId::new("bundle").unwrap(),
&ReleaseId::new("r1").unwrap(),
)
.is_err());
Ok::<_, &str>(())
},
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
assert!(receipt.post_commit_failure().is_none());
}
#[test]
fn post_commit_keep_installed_records_bounded_failure() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let validated = prepared
.verify_integrity()
.unwrap()
.validate(&AllValidators::new())
.unwrap();
let receipt = validated
.commit_with_post_commit(
CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
PostCommitFailurePolicy::KeepInstalled,
|| Err("é".repeat(400)),
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
let failure = receipt.post_commit_failure().unwrap();
assert_eq!(failure.phase(), super::FailurePhase::PostCommit);
assert!(failure.detail().len() <= 512);
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"new-main"
);
assert_eq!(
fs::read(install.path().join("bin/helper")).unwrap(),
b"new-helper"
);
}
#[test]
fn post_commit_rollback_restores_old_generation_and_absent_members() {
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
let main = inputs.write_file("main", b"new-main").unwrap();
let helper = inputs.write_file("helper", b"new-helper").unwrap();
install.write_file("bin/main", b"old-main").unwrap();
let artifacts = ArtifactSet::new(vec![
ArtifactMember::new(MemberId::new("main").unwrap(), main, "bin/main")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(
super::hash_file(&inputs.path().join("main")).unwrap(),
)),
ArtifactMember::new(MemberId::new("helper").unwrap(), helper, "bin/helper")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(
super::hash_file(&inputs.path().join("helper")).unwrap(),
)),
])
.unwrap();
let prepared = InstallPlan::new(
ProductId::new("bundle").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
artifacts,
)
.unwrap()
.prepare()
.unwrap();
let verifier = allow_create();
let receipt = prepared
.verify_integrity()
.unwrap()
.validate(&AllValidators::new())
.unwrap()
.commit_with_post_commit(
CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
PostCommitFailurePolicy::RollBack,
|| Err("health check failed"),
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::PostCommit
);
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"old-main"
);
assert!(!install.path().join("bin/helper").exists());
}
#[test]
fn post_commit_rollback_failure_retains_both_reports_and_evidence() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let receipt = prepared
.verify_integrity()
.unwrap()
.validate(&AllValidators::new())
.unwrap()
.commit_with_post_commit_fault(
CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
PostCommitFailurePolicy::RollBack,
CommitFault::PostCommitRollback,
)
.unwrap();
assert_eq!(
receipt.disposition(),
TransactionDisposition::RecoveryRequired
);
assert_eq!(
receipt.post_commit_failure().unwrap().phase(),
super::FailurePhase::PostCommit
);
assert_eq!(
receipt.rollback_failure().unwrap().phase(),
super::FailurePhase::Rollback
);
assert!(receipt.recovery_path().unwrap().exists());
let recovery = receipt.recovery_path().unwrap().to_path_buf();
let _ = fs::remove_dir_all(recovery);
let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
}
#[test]
fn keep_installed_finalize_failure_preserves_both_failure_facts() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let receipt = prepared
.verify_integrity()
.unwrap()
.validate(&AllValidators::new())
.unwrap()
.commit_with_post_commit_fault(
CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
PostCommitFailurePolicy::KeepInstalled,
CommitFault::Finalize,
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::Finalize
);
assert_eq!(
receipt.post_commit_failure().unwrap().phase(),
super::FailurePhase::PostCommit
);
let recovery = receipt.recovery_path().unwrap().to_path_buf();
assert!(recovery.exists());
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"new-main"
);
let _ = fs::remove_dir_all(recovery);
let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
}
#[test]
fn post_commit_panic_is_reported_and_rolls_back() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let receipt = prepared
.verify_integrity()
.unwrap()
.validate(&AllValidators::new())
.unwrap()
.commit_with_post_commit(
CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
PostCommitFailurePolicy::RollBack,
|| -> Result<(), &str> { panic!("simulated check panic") },
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
receipt.post_commit_failure().unwrap().detail(),
"post-commit check panicked"
);
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"old-main"
);
}
#[test]
fn partial_commit_failure_restores_every_old_member() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let receipt = commit_verified_with_fault(
prepared,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::Commit(MemberId::new("helper").unwrap()),
)
.expect("receipt");
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert!(receipt.rollback_performed());
assert!(receipt.rollback_verified());
let failure = receipt.failure().expect("failure report");
assert_eq!(failure.phase(), super::FailurePhase::Commit);
assert_eq!(failure.member(), Some(&MemberId::new("helper").unwrap()));
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"old-main"
);
assert_eq!(
fs::read(install.path().join("bin/helper")).unwrap(),
b"old-helper"
);
}
#[test]
fn rollback_removes_new_members_that_were_absent_before_commit() {
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
let main_src = inputs.write_file("main", b"new-main").unwrap();
let helper_src = inputs.write_file("helper", b"new-helper").unwrap();
let main_digest = super::hash_file(&main_src).unwrap();
let helper_digest = super::hash_file(&helper_src).unwrap();
install.write_file("bin/main", b"old-main").unwrap();
let prepared = InstallPlan::new(
ProductId::new("bundle").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::new(vec![
ArtifactMember::new(
MemberId::new("main").unwrap(),
inputs.path().join("main"),
"bin/main",
)
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
ArtifactMember::new(
MemberId::new("helper").unwrap(),
inputs.path().join("helper"),
"bin/helper",
)
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
])
.unwrap(),
)
.unwrap()
.prepare()
.unwrap();
let verifier = allow_create();
let receipt = commit_verified_with_fault(
prepared,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::Commit(MemberId::new("helper").unwrap()),
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"old-main"
);
assert!(!install.path().join("bin/helper").exists());
}
#[test]
fn lock_contention_and_malformed_lock_fail_closed() {
let (_inputs, install, prepared) = prepared_bundle(false);
let product = ProductId::new("bundle").unwrap();
let release = ReleaseId::new("r1").unwrap();
let _lock = MutationLock::acquire(install.path(), &product, &release).unwrap();
let verifier = allow_create();
let error = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap_err();
assert!(matches!(error, Error::UpdateInProgress { .. }));
drop(_lock);
let (_inputs, install2, prepared) = prepared_bundle(false);
let lock = install2.path().join(".eggup-mutation.lock");
fs::write(&lock, b"malformed").unwrap();
let error = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap_err();
assert!(matches!(error, Error::UpdateInProgress { .. }));
assert!(lock.exists());
}
#[test]
fn rollback_failure_returns_recovery_required_and_retains_evidence() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let receipt = commit_verified_with_fault(
prepared,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::CommitThenRollback(
MemberId::new("helper").unwrap(),
MemberId::new("main").unwrap(),
),
)
.unwrap();
assert_eq!(
receipt.disposition(),
TransactionDisposition::RecoveryRequired
);
assert!(!receipt.rollback_verified());
assert!(receipt.recovery_path().is_some());
assert!(receipt.failure().is_some());
assert!(receipt.rollback_failure().is_some());
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::Commit
);
assert_eq!(
receipt.rollback_failure().unwrap().phase(),
super::FailurePhase::Rollback
);
assert!(install.path().join(".eggup-mutation.lock").exists());
let recovery = receipt.recovery_path().unwrap();
assert!(recovery.exists());
let _ = fs::remove_dir_all(recovery);
let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
}
#[test]
fn self_update_rollback_failure_retains_the_real_old_image() {
use super::{hash_file, CurrentExecutable, IntegrityRequirement, StagePlacement};
let inputs = InstallationRoot::new().expect("inputs");
let install = InstallationRoot::new().expect("install");
let installed = install.path().join("app");
fs::write(&installed, b"old-generation").expect("install old");
let candidate = inputs
.write_file("candidate", b"new-generation")
.expect("candidate");
let current = CurrentExecutable::bind(&installed).expect("bind current executable");
let digest = hash_file(&candidate).expect("hash candidate");
let plan = InstallPlan::for_current_executable(
ProductId::new("app").unwrap(),
ReleaseId::new("r1").unwrap(),
&candidate,
¤t,
IntegrityRequirement::Sha256(digest),
)
.expect("plan");
assert_eq!(
plan.stage_placement(),
StagePlacement::InsideInstallationRoot
);
let prepared = plan.prepare().expect("prepare");
let receipt = prepared
.verify_integrity()
.expect("verify")
.validate(&AllValidators::new())
.expect("validate")
.commit_with_post_commit_fault(
CommitOwnership::new(&FixedVerifier(Ownership::Owned), AbsentPolicy::DenyCreate),
PostCommitFailurePolicy::RollBack,
CommitFault::PostCommitRollback,
)
.expect("receipt");
assert_eq!(
receipt.disposition(),
TransactionDisposition::RecoveryRequired,
"an incomplete rollback is never reported as RolledBack"
);
assert!(!receipt.rollback_verified());
assert!(receipt.rollback_failure().is_some());
let recovery = receipt
.recovery_path()
.expect("the real retained path must be reported");
assert!(
recovery.exists(),
"reported recovery path must really exist"
);
assert!(
recovery.starts_with(fs::canonicalize(install.path()).expect("canonical install root")),
"recovery evidence stays inside the executable's own directory: {}",
recovery.display()
);
let _ = fs::remove_dir_all(recovery);
let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
}
#[test]
fn precommit_and_backup_failures_restore_old_state() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let receipt = commit_verified_with_fault(
prepared,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::BeforeFirstCommit,
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert!(receipt.failure().is_some());
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"old-main"
);
let (_inputs, install, prepared) = prepared_bundle(true);
let receipt = commit_verified_with_fault(
prepared,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::Backup(MemberId::new("helper").unwrap()),
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::Backup
);
assert_eq!(
fs::read(install.path().join("bin/helper")).unwrap(),
b"old-helper"
);
}
#[test]
fn injected_lock_creation_failure_performs_no_mutation() {
let (_inputs, install, prepared) = prepared_bundle(true);
let verifier = allow_create();
let error = commit_verified_with_fault(
prepared,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::LockCreation,
)
.unwrap_err();
assert!(matches!(error, Error::Injected(_)));
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"old-main"
);
assert!(!install.path().join(".eggup-mutation.lock").exists());
}
#[cfg(unix)]
#[test]
fn destination_links_fail_immediately_before_backup() {
use std::fs::hard_link;
use std::os::unix::fs::symlink;
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
let src = inputs.write_file("main", b"new-main").unwrap();
let digest = super::hash_file(&src).unwrap();
install.write_file("bin/real", b"old-main").unwrap();
symlink(install.path().join("real"), install.path().join("bin/main")).unwrap();
let prepared = InstallPlan::new(
ProductId::new("bundle").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(
ArtifactMember::new(
MemberId::new("main").unwrap(),
inputs.path().join("main"),
"bin/main",
)
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(digest)),
)
.unwrap(),
)
.unwrap()
.prepare()
.unwrap();
let verifier = allow_create();
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::Ownership
);
assert_eq!(
fs::read(install.path().join("bin/real")).unwrap(),
b"old-main"
);
let (_inputs, install, prepared) = prepared_bundle(true);
hard_link(
install.path().join("bin/main"),
install.path().join("bin/linked"),
)
.unwrap();
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
fs::read(install.path().join("bin/main")).unwrap(),
b"old-main"
);
}
#[test]
fn hashes_known_bytes_and_parses_strict_sidecars() {
let fixture = InstallationRoot::new().unwrap();
let path = fixture.write_file("abc", b"abc").unwrap();
let manifest = super::parse_sha256_sidecar(
"BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD abc\n",
)
.unwrap();
assert_eq!(manifest.filename(), Some("abc"));
assert_eq!(
super::verify_file(&path, &manifest).unwrap(),
*manifest.digest()
);
assert!(super::parse_sha256_sidecar("abc\ndef").is_err());
assert!(super::parse_sha256_sidecar(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad other"
)
.and_then(|manifest| super::verify_file(&path, &manifest))
.is_err());
assert!(super::parse_sha256_sidecar("not-a-digest abc").is_err());
}
#[test]
fn integrity_verification_precedes_candidate_validation() {
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
let source = inputs.write_file("main", b"verified bytes").unwrap();
let digest = super::hash_file(&source).unwrap();
let member = ArtifactMember::new(MemberId::new("main").unwrap(), source, "bin/main")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(digest));
let verified = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap()
.prepare()
.unwrap()
.verify_integrity()
.unwrap();
assert_eq!(
verified
.integrity(&MemberId::new("main").unwrap())
.unwrap()
.status(),
super::IntegrityStatus::Verified
);
let bad_source = inputs.write_file("bad", b"bad bytes").unwrap();
let bad_member = ArtifactMember::new(MemberId::new("bad").unwrap(), bad_source, "bin/bad")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(digest));
assert!(matches!(
InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(bad_member).unwrap(),
)
.unwrap()
.prepare()
.unwrap()
.verify_integrity(),
Err(Error::VerificationFailed(_))
));
let no_requirement = ArtifactMember::new(
MemberId::new("none").unwrap(),
inputs.write_file("none", b"no declaration").unwrap(),
"bin/none",
)
.unwrap();
let verified = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(no_requirement).unwrap(),
)
.unwrap()
.prepare()
.unwrap()
.verify_integrity()
.unwrap();
let validator = ExactIdentityValidator::new(MemberId::new("none").unwrap(), "anything");
assert!(matches!(
verified.validate(&validator),
Err(Error::VerificationFailed(_))
));
}
#[cfg(unix)]
#[test]
fn bounded_candidates_are_exact_and_environment_is_cleared() {
use std::os::unix::fs::PermissionsExt;
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
let script = inputs
.write_file("main", b"#!/bin/sh\nprintf 'eggup 1.2.3\\n'\n")
.unwrap();
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
let digest = super::hash_file(&script).unwrap();
let member = ArtifactMember::new(MemberId::new("main").unwrap(), script, "bin/main")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(digest));
fs::create_dir_all(install.path().join("bin")).unwrap();
let verified = InstallPlan::new(
ProductId::new("eggup").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap()
.prepare()
.unwrap()
.verify_integrity()
.unwrap();
let validator =
ExactIdentityValidator::new(MemberId::new("main").unwrap(), "eggup 1.2.3\n");
let validated = verified.validate(&validator).unwrap();
let verifier = allow_create();
assert_eq!(
validated
.commit(CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate))
.unwrap()
.disposition(),
TransactionDisposition::Committed
);
let secret_script = inputs
.write_file(
"secret",
b"#!/bin/sh\nprintf '%s' \"${EGGUP_SECRET-unset}\"\n",
)
.unwrap();
fs::set_permissions(&secret_script, fs::Permissions::from_mode(0o755)).unwrap();
let output = super::run_bounded(&super::CommandSpec::new(secret_script)).unwrap();
assert!(output.success(), "bounded candidate output: {output:?}");
assert_eq!(output.stdout(), b"unset");
}
#[cfg(unix)]
#[test]
fn bounded_runner_kills_timeouts_and_limits_output() {
use std::os::unix::fs::PermissionsExt;
let fixture = InstallationRoot::new().unwrap();
let timeout_script = fixture
.write_file("timeout", b"#!/bin/sh\nsleep 2\n")
.unwrap();
let noisy_script = fixture
.write_file("noisy", b"#!/bin/sh\nprintf '1234567890'\n")
.unwrap();
for path in [&timeout_script, &noisy_script] {
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
}
let timed_out = super::run_bounded(
&super::CommandSpec::new(timeout_script).timeout(Duration::from_millis(20)),
)
.unwrap();
assert!(timed_out.timed_out());
assert!(!timed_out.success());
let limited =
super::run_bounded(&super::CommandSpec::new(noisy_script).max_output_bytes(4)).unwrap();
assert!(limited.output_limited());
assert!(!limited.success());
}
#[cfg(unix)]
#[test]
fn cross_member_identity_requires_bundle_agreement() {
use std::os::unix::fs::PermissionsExt;
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
let main = inputs
.write_file("main", b"#!/bin/sh\nprintf 'bundle 9\\n'\n")
.unwrap();
let helper = inputs
.write_file("helper", b"#!/bin/sh\nprintf 'bundle 9\\n'\n")
.unwrap();
for path in [&main, &helper] {
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
}
let main_digest = super::hash_file(&main).unwrap();
let helper_digest = super::hash_file(&helper).unwrap();
let verified = InstallPlan::new(
ProductId::new("bundle").unwrap(),
ReleaseId::new("r9").unwrap(),
install.path(),
ArtifactSet::new(vec![
ArtifactMember::new(MemberId::new("main").unwrap(), main, "bin/main")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
ArtifactMember::new(MemberId::new("helper").unwrap(), helper, "bin/helper")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
])
.unwrap(),
)
.unwrap()
.prepare()
.unwrap()
.verify_integrity()
.unwrap();
let validator = super::CrossMemberAgreementValidator::new(
[
MemberId::new("main").unwrap(),
MemberId::new("helper").unwrap(),
],
"bundle 9\n",
);
assert!(verified.validate(&validator).is_ok());
}
fn single_verified(
new_bytes: &[u8],
old_bytes: Option<&[u8]>,
with_bin_dir: bool,
) -> (
InstallationRoot,
InstallationRoot,
super::PreparedTransaction,
[u8; 32],
) {
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
let src = inputs.write_file("main", new_bytes).unwrap();
let digest = super::hash_file(&src).unwrap();
if let Some(old) = old_bytes {
install.write_file("bin/main", old).unwrap();
} else if with_bin_dir {
fs::create_dir_all(install.path().join("bin")).unwrap();
}
let member = ArtifactMember::new(MemberId::new("main").unwrap(), src, "bin/main")
.unwrap()
.with_integrity(super::IntegrityRequirement::Sha256(digest));
let prepared = InstallPlan::new(
ProductId::new("p").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap()
.prepare()
.unwrap();
(inputs, install, prepared, digest)
}
#[test]
fn ownership_owned_allows_replacement_and_reports_no_failure() {
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let old_digest = super::hash_file(&install.path().join("bin/main")).unwrap();
let verifier = ExactDigestVerifier::new(vec![(MemberId::new("main").unwrap(), old_digest)]);
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"new");
}
#[test]
fn ownership_foreign_and_unknown_fail_closed_with_zero_mutation() {
for fixed in [Ownership::Foreign, Ownership::Unknown] {
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let verifier = FixedVerifier(fixed);
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::Ownership
);
assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
}
}
#[test]
fn ownership_absent_create_policy_is_enforced() {
let (_i, install, prepared, _) = single_verified(b"new", None, true);
let verifier = AbsentOnlyVerifier;
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"new");
let (_i, install, prepared, _) = single_verified(b"new", None, true);
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert!(!install.path().join("bin/main").exists());
}
#[test]
fn ownership_flapping_between_preflight_and_lock_fails() {
use std::cell::Cell;
#[derive(Debug)]
struct Flap {
first: Ownership,
second: Ownership,
calls: Cell<usize>,
}
impl OwnershipVerifier for Flap {
fn verify(&self, _m: &MemberId, _d: &Path) -> Ownership {
let n = self.calls.get();
self.calls.set(n + 1);
if n == 0 {
self.first
} else {
self.second
}
}
}
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let verifier = Flap {
first: Ownership::Owned,
second: Ownership::Foreign,
calls: Cell::new(0),
};
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
}
#[test]
fn missing_parent_fails_without_creating_directories() {
let (_i, install, prepared, _) = single_verified(b"new", None, false);
assert!(!install.path().join("bin").exists());
let verifier = allow_create();
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert!(!install.path().join("bin").exists());
assert!(!install.path().join("bin/main").exists());
}
#[cfg(unix)]
#[test]
fn symlinked_parent_fails_closed() {
use std::os::unix::fs::symlink;
let (_i, install, prepared, _) = single_verified(b"new", None, false);
fs::create_dir_all(install.path().join("real-bin")).unwrap();
symlink(install.path().join("real-bin"), install.path().join("bin")).unwrap();
let verifier = allow_create();
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
}
#[test]
fn staged_mutation_after_validation_fails_before_live_mutation() {
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let verified = prepared.verify_integrity().unwrap();
let validated = verified.validate(&AllValidators::new()).unwrap();
let staged = validated
.staged_path(&MemberId::new("main").unwrap())
.unwrap();
fs::write(&staged, b"tampered").unwrap();
let verifier = allow_create();
let receipt = validated
.commit(CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate))
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::StageRevalidation
);
assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
}
#[test]
fn unverified_integrity_none_cannot_commit() {
let inputs = InstallationRoot::new().unwrap();
let install = InstallationRoot::new().unwrap();
fs::create_dir_all(install.path().join("bin")).unwrap();
let src = inputs.write_file("main", b"bytes").unwrap();
let member = ArtifactMember::new(MemberId::new("main").unwrap(), src, "bin/main").unwrap();
let prepared = InstallPlan::new(
ProductId::new("p").unwrap(),
ReleaseId::new("r1").unwrap(),
install.path(),
ArtifactSet::single(member).unwrap(),
)
.unwrap()
.prepare()
.unwrap();
let verified = prepared.verify_integrity().unwrap();
assert!(verified.validate(&AllValidators::new()).is_err());
}
#[test]
fn cleanup_failure_reports_real_backup_root() {
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let verifier = allow_create();
let receipt = commit_verified_with_fault(
prepared,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::Finalize,
)
.unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
assert_eq!(receipt.cleanup(), CleanupDisposition::RetainedForRecovery);
let path = receipt.recovery_path().unwrap();
assert!(path.exists());
assert!(!path.to_string_lossy().contains("cleanup-failed"));
assert_eq!(
receipt.failure().unwrap().phase(),
super::FailurePhase::Finalize
);
let _ = fs::remove_dir_all(path);
let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
}
#[cfg(unix)]
#[test]
fn transaction_owned_state_is_owner_private() {
use std::os::unix::fs::PermissionsExt;
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let stage_mode = fs::metadata(prepared.stage_root())
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(stage_mode, 0o700);
let staged = prepared
.staged_path(&MemberId::new("main").unwrap())
.unwrap();
let file_mode = fs::metadata(&staged).unwrap().permissions().mode() & 0o777;
assert_eq!(file_mode, 0o600);
let verifier = allow_create();
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
let (_i2, install2, prepared2, _) = single_verified(b"n2", Some(b"o2"), false);
let receipt = commit_verified_with_fault(
prepared2,
&verifier,
AbsentPolicy::AllowCreate,
CommitFault::Finalize,
)
.unwrap();
let lock_mode = fs::metadata(install2.path().join(".eggup-mutation.lock"))
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(lock_mode, 0o600);
let _ = fs::remove_dir_all(receipt.recovery_path().unwrap());
let _ = fs::remove_file(install2.path().join(".eggup-mutation.lock"));
let _ = install;
}
#[test]
fn lock_inspect_never_deletes_and_reports_status() {
let install = InstallationRoot::new().unwrap();
assert_eq!(
MutationLock::inspect(install.path()).unwrap(),
super::LockStatus::Available
);
let product = ProductId::new("p").unwrap();
let release = ReleaseId::new("r").unwrap();
let lock = MutationLock::acquire(install.path(), &product, &release).unwrap();
match MutationLock::inspect(install.path()).unwrap() {
super::LockStatus::Held { contents, .. } => assert!(contents.is_some()),
other => panic!("expected held, got {other:?}"),
}
drop(lock);
assert_eq!(
MutationLock::inspect(install.path()).unwrap(),
super::LockStatus::Available
);
fs::write(install.path().join(".eggup-mutation.lock"), b"malformed").unwrap();
match MutationLock::inspect(install.path()).unwrap() {
super::LockStatus::Held { .. } | super::LockStatus::Malformed { .. } => {}
other => panic!("expected held/malformed, got {other:?}"),
}
assert!(install.path().join(".eggup-mutation.lock").exists());
}
#[test]
fn oversized_lock_record_is_malformed_not_deleted() {
let install = InstallationRoot::new().unwrap();
let big = vec![b'x'; 8192];
fs::write(install.path().join(".eggup-mutation.lock"), &big).unwrap();
match MutationLock::inspect(install.path()).unwrap() {
super::LockStatus::Malformed { .. } => {}
other => panic!("expected malformed, got {other:?}"),
}
assert!(install.path().join(".eggup-mutation.lock").exists());
}
#[test]
fn exact_digest_verifier_proves_ownership() {
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let live_digest = super::hash_file(&install.path().join("bin/main")).unwrap();
let verifier =
ExactDigestVerifier::new(vec![(MemberId::new("main").unwrap(), live_digest)]);
assert_eq!(
verifier.verify(
&MemberId::new("main").unwrap(),
&install.path().join("bin/main")
),
Ownership::Owned
);
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
}
#[test]
fn absent_only_verifier_never_authorizes_replacement() {
let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let verifier = AbsentOnlyVerifier;
assert_eq!(
verifier.verify(
&MemberId::new("main").unwrap(),
&install.path().join("bin/main")
),
Ownership::Foreign
);
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
}
#[test]
fn failure_reports_carry_phase_category_and_member() {
let (_i, _install, prepared, _) = single_verified(b"new", Some(b"old"), false);
let verifier = FixedVerifier(Ownership::Foreign);
let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
let failure = receipt.failure().unwrap();
assert_eq!(failure.phase(), super::FailurePhase::Ownership);
assert!(!failure.detail().is_empty());
assert!(failure.detail().len() <= 512);
}
#[test]
fn checksum_only_state_is_explicit_in_docs() {
let desc = env!("CARGO_PKG_DESCRIPTION");
let _ = desc;
}
#[allow(dead_code)]
fn _unused() {
let _m: HashMap<MemberId, [u8; 32]> = HashMap::new();
}
#[test]
fn competing_writer_after_the_claim_is_preserved() {
use crate::lock::POST_CLAIM_HOOK;
let root = InstallationRoot::new().expect("root");
let lock_path = root.path().join(".eggup-mutation.lock");
fs::write(&lock_path, "pid=1 nonce=1 product=a release=b\n").expect("plant");
let competing = "pid=2 nonce=2 product=a release=b\n".to_string();
struct Proven;
impl std::fmt::Debug for Proven {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Proven")
}
}
impl crate::lock::StaleLockVerifier for Proven {
fn classify(
&self,
_observed: &crate::lock::LockObservation,
) -> crate::lock::StaleLockDecision {
crate::lock::StaleLockDecision::ProvenStale
}
}
let hook_ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let hook_ran_in_hook = hook_ran.clone();
*POST_CLAIM_HOOK.lock().expect("hook slot") = Some(Box::new(move || {
fs::write(&lock_path, &competing).expect("competing writer");
hook_ran_in_hook.store(true, std::sync::atomic::Ordering::SeqCst);
}));
let error = MutationLock::acquire_with_recovery(
root.path(),
&ProductId::new("a").unwrap(),
&ReleaseId::new("b").unwrap(),
&Proven,
)
.expect_err("the competing writer owns the domain");
assert!(
matches!(
error,
crate::error::RecoveryError::Core(Error::UpdateInProgress { .. })
),
"expected typed contention, got {error:?}"
);
assert!(
hook_ran.load(std::sync::atomic::Ordering::SeqCst),
"the injected competing writer must actually have run, otherwise this \
test is asserting the pre-claim rejection path and proves nothing \
about the post-claim branch"
);
assert_eq!(
fs::read_to_string(root.path().join(".eggup-mutation.lock")).expect("read"),
"pid=2 nonce=2 product=a release=b\n",
"the winning writer's record must survive byte-identical"
);
let leftovers: Vec<_> = fs::read_dir(root.path())
.expect("read dir")
.flatten()
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.filter(|name| name.starts_with(".eggup-stale-claim-"))
.collect();
assert!(
leftovers.is_empty(),
"our own displaced record must be cleaned up: {leftovers:?}"
);
}
#[test]
fn restore_never_clobbers_a_lock_taken_in_the_meantime() {
let root = InstallationRoot::new().expect("root");
let lock_path = root.path().join(".eggup-mutation.lock");
let claim = root.path().join(".eggup-stale-claim-test");
fs::write(&claim, "pid=1 nonce=1 product=a release=b\n").expect("write claim");
let winner = "pid=9 nonce=9 product=other release=z\n";
fs::write(&lock_path, winner).expect("write winner");
let error =
crate::lock::restore_claim(&claim, &lock_path, "pid=1 nonce=1 product=a release=b\n");
assert!(
matches!(error, crate::error::RecoveryError::RecoveryRequired { .. }),
"a re-taken lock path must retain the claim, got {error:?}"
);
assert_eq!(
fs::read_to_string(&lock_path).expect("read"),
winner,
"the competing writer's record must be untouched"
);
assert!(
claim.exists(),
"the displaced record is retained and reported, not deleted"
);
}
}