1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3#![doc = "Policy-neutral local mechanics for verified multi-artifact updates."]
4#![doc = ""]
5#![doc = "The core crate deliberately does not fetch bytes, manage services, or choose release policy."]
6#![doc = "Callers acquire artifacts and prove destination ownership; the core"]
7#![doc = "provides validated preparation, SHA-256 integrity verification, bounded"]
8#![doc = "candidate validation, locked ownership revalidation, staged-digest"]
9#![doc = "revalidation, and atomic-feeling multi-artifact commit with rollback."]
10#![doc = ""]
11#![doc = "Integrity here is checksum evidence only. No authenticity or signature"]
12#![doc = "claim is made: there is no authenticity verifier in this crate."]
13#![doc = ""]
14#![doc = "Runnable flows live in `examples/`: one-member, multi-member, custom"]
15#![doc = "validator, ownership verifier, and receipt interpretation."]
16
17mod candidate;
18mod domain;
19mod error;
20mod integrity;
21mod lock;
22mod stage;
23mod transaction;
24
25pub use candidate::{
26 run_bounded, AllValidators, CandidateValidator, CommandOutput, CommandSpec,
27 CrossMemberAgreementValidator, ExactIdentityValidator, ValidatedTransaction,
28};
29pub use domain::{
30 AbsentOnlyVerifier, AbsentPolicy, ArtifactMember, ArtifactSet, CommitOwnership,
31 ExactDigestVerifier, ExistingAsOwnedVerifier, FileKind, InstallPlan, IntegrityRequirement,
32 MemberId, Ownership, OwnershipVerifier, PermissionsIntent, ProductId, ReleaseId,
33};
34pub use error::{Error, Result};
35pub use integrity::{
36 hash_file, parse_sha256_sidecar, verify_file, IntegrityResult, IntegrityStatus, Sha256Manifest,
37 VerifiedTransaction,
38};
39pub use lock::{LockStatus, MutationLock};
40pub use stage::PreparedTransaction;
41pub use transaction::{
42 CleanupDisposition, FailureCategory, FailurePhase, FailureReport, PostCommitFailurePolicy,
43 TransactionDisposition, TransactionReceipt,
44};
45
46#[cfg(test)]
47mod test_support;
48
49#[cfg(test)]
50mod tests {
51 use super::test_support::{FailureInjector, FailurePoint, InstallationRoot};
52 use super::transaction::CommitFault;
53 use super::{
54 AbsentOnlyVerifier, AbsentPolicy, AllValidators, ArtifactMember, ArtifactSet,
55 CleanupDisposition, CommitOwnership, Error, ExactDigestVerifier, ExactIdentityValidator,
56 ExistingAsOwnedVerifier, InstallPlan, MemberId, MutationLock, Ownership, OwnershipVerifier,
57 PostCommitFailurePolicy, ProductId, ReleaseId, TransactionDisposition,
58 };
59 use std::collections::HashMap;
60 use std::fs;
61 use std::path::Path;
62 use std::time::Duration;
63
64 struct FixedVerifier(Ownership);
65 impl std::fmt::Debug for FixedVerifier {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_tuple("FixedVerifier").field(&self.0).finish()
68 }
69 }
70 impl OwnershipVerifier for FixedVerifier {
71 fn verify(&self, _m: &MemberId, _d: &Path) -> Ownership {
72 self.0
73 }
74 }
75
76 fn allow_create() -> ExistingAsOwnedVerifier {
77 ExistingAsOwnedVerifier
78 }
79
80 fn commit_verified(
81 prepared: super::PreparedTransaction,
82 verifier: &dyn OwnershipVerifier,
83 absent: AbsentPolicy,
84 ) -> super::Result<super::TransactionReceipt> {
85 let verified = prepared.verify_integrity()?;
86 let validated = verified.validate(&AllValidators::new())?;
87 validated.commit(CommitOwnership::new(verifier, absent))
88 }
89
90 fn commit_verified_with_fault(
91 prepared: super::PreparedTransaction,
92 verifier: &dyn OwnershipVerifier,
93 absent: AbsentPolicy,
94 fault: CommitFault,
95 ) -> super::Result<super::TransactionReceipt> {
96 let verified = prepared.verify_integrity()?;
97 let validated = verified.validate(&AllValidators::new())?;
98 validated.commit_with_fault(CommitOwnership::new(verifier, absent), fault)
99 }
100
101 #[test]
102 fn fixture_writes_and_reads_exact_bytes() {
103 let fixture = InstallationRoot::new().expect("fixture");
104 fixture
105 .write_file("bin/eggup", b"exact bytes")
106 .expect("write");
107
108 assert_eq!(
109 fixture.read_file("bin/eggup").expect("read"),
110 b"exact bytes"
111 );
112 assert!(fixture.path().join("bin/eggup").is_file());
113 }
114
115 #[test]
116 fn fixture_rejects_escape_paths() {
117 let fixture = InstallationRoot::new().expect("fixture");
118
119 let error = fixture.write_file("../outside", b"nope").unwrap_err();
120
121 assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
122 assert!(!fixture.path().parent().unwrap().join("outside").exists());
123 }
124
125 #[test]
126 fn failure_injector_defaults_to_no_failure() {
127 let mut injector = FailureInjector::default();
128
129 assert!(injector.check(FailurePoint::Prepare).is_ok());
130 assert!(injector.check(FailurePoint::Commit).is_ok());
131 }
132
133 #[test]
134 fn prepares_a_multi_member_bundle_without_touching_destinations() {
135 let inputs = InstallationRoot::new().expect("inputs");
136 let install = InstallationRoot::new().expect("install");
137 inputs.write_file("egress", b"egress").expect("write");
138 inputs.write_file("helper", b"helper").expect("write");
139 let product = ProductId::new("egress").expect("product");
140 let release = ReleaseId::new("2026.09.22").expect("release");
141 let members = ArtifactSet::new(vec![
142 ArtifactMember::new(
143 MemberId::new("main").expect("member"),
144 inputs.path().join("egress"),
145 "bin/egress",
146 )
147 .expect("artifact"),
148 ArtifactMember::new(
149 MemberId::new("helper").expect("member"),
150 inputs.path().join("helper"),
151 "bin/helper",
152 )
153 .expect("artifact"),
154 ])
155 .expect("members");
156
157 let prepared = InstallPlan::new(product, release, install.path(), members)
158 .expect("plan")
159 .prepare()
160 .expect("prepare");
161
162 assert!(!install.path().join("bin/egress").exists());
163 assert_eq!(
164 fs::read(
165 prepared
166 .staged_path(&MemberId::new("main").unwrap())
167 .unwrap()
168 )
169 .unwrap(),
170 b"egress"
171 );
172 }
173
174 #[test]
175 fn preparation_failure_cleans_private_stage() {
176 let inputs = InstallationRoot::new().expect("inputs");
177 let install = InstallationRoot::new().expect("install");
178 inputs.write_file("eggup", b"bytes").expect("write");
179 let plan = InstallPlan::new(
180 ProductId::new("eggup").unwrap(),
181 ReleaseId::new("r1").unwrap(),
182 install.path(),
183 ArtifactSet::single(
184 ArtifactMember::new(
185 MemberId::new("main").unwrap(),
186 inputs.path().join("eggup"),
187 "eggup",
188 )
189 .unwrap(),
190 )
191 .unwrap(),
192 )
193 .unwrap();
194
195 assert!(plan
196 .clone()
197 .prepare_with_failure(FailurePoint::StageCreate)
198 .is_err());
199 let result = plan.prepare_with_failure(FailurePoint::StageCopy);
200
201 assert!(result.is_err());
202 assert!(!install.path().join("eggup").exists());
203 let stage_prefix = format!(
204 ".eggup-stage-{}-",
205 install.path().file_name().unwrap().to_string_lossy()
206 );
207 assert!(install
208 .path()
209 .parent()
210 .unwrap()
211 .read_dir()
212 .unwrap()
213 .filter_map(std::result::Result::ok)
214 .all(|entry| !entry
215 .file_name()
216 .to_string_lossy()
217 .starts_with(&stage_prefix)));
218 }
219
220 #[test]
221 fn rejects_duplicate_normalized_destinations() {
222 let inputs = InstallationRoot::new().expect("inputs");
223 let install = InstallationRoot::new().expect("install");
224 inputs.write_file("one", b"one").expect("write");
225 inputs.write_file("two", b"two").expect("write");
226 let members = ArtifactSet::new(vec![
227 ArtifactMember::new(
228 MemberId::new("one").unwrap(),
229 inputs.path().join("one"),
230 "bin/./app",
231 )
232 .unwrap(),
233 ArtifactMember::new(
234 MemberId::new("two").unwrap(),
235 inputs.path().join("two"),
236 "bin/app",
237 )
238 .unwrap(),
239 ])
240 .unwrap();
241
242 let result = InstallPlan::new(
243 ProductId::new("eggup").unwrap(),
244 ReleaseId::new("r1").unwrap(),
245 install.path(),
246 members,
247 );
248
249 assert!(result.is_err());
250 }
251
252 #[test]
253 fn rejects_empty_sets_and_escaping_destinations() {
254 assert!(ArtifactSet::new(Vec::new()).is_err());
255 assert!(ArtifactMember::new(
256 MemberId::new("main").unwrap(),
257 "/tmp/acquired",
258 "../outside",
259 )
260 .is_err());
261 assert!(ArtifactMember::new(
262 MemberId::new("main").unwrap(),
263 "/tmp/acquired",
264 "/absolute/path",
265 )
266 .is_err());
267 }
268
269 #[cfg(unix)]
270 #[test]
271 fn rejects_symlink_sources_and_preserves_executable_intent_in_stage() {
272 use std::os::unix::fs::symlink;
273
274 let inputs = InstallationRoot::new().expect("inputs");
275 let install = InstallationRoot::new().expect("install");
276 inputs.write_file("real", b"real").expect("write");
277 symlink(inputs.path().join("real"), inputs.path().join("link")).expect("symlink");
278 let symlink_member = ArtifactMember::new(
279 MemberId::new("link").unwrap(),
280 inputs.path().join("link"),
281 "bin/link",
282 )
283 .unwrap();
284 assert!(InstallPlan::new(
285 ProductId::new("eggup").unwrap(),
286 ReleaseId::new("r1").unwrap(),
287 install.path(),
288 ArtifactSet::single(symlink_member).unwrap(),
289 )
290 .is_err());
291
292 let member = ArtifactMember::new(
293 MemberId::new("real").unwrap(),
294 inputs.path().join("real"),
295 "bin/real",
296 )
297 .unwrap()
298 .with_permissions(super::PermissionsIntent::Executable);
299 let prepared = InstallPlan::new(
300 ProductId::new("eggup").unwrap(),
301 ReleaseId::new("r1").unwrap(),
302 install.path(),
303 ArtifactSet::single(member).unwrap(),
304 )
305 .unwrap()
306 .prepare()
307 .unwrap();
308 #[cfg(unix)]
309 {
310 use std::os::unix::fs::PermissionsExt;
311 let mode = fs::metadata(
312 prepared
313 .staged_path(&MemberId::new("real").unwrap())
314 .unwrap(),
315 )
316 .unwrap()
317 .permissions()
318 .mode();
319 assert_ne!(mode & 0o111, 0);
320 assert_eq!(mode & 0o777, 0o700);
321 }
322 }
323
324 fn prepared_bundle(
325 existing: bool,
326 ) -> (
327 InstallationRoot,
328 InstallationRoot,
329 super::PreparedTransaction,
330 ) {
331 let inputs = InstallationRoot::new().expect("inputs");
332 let install = InstallationRoot::new().expect("install");
333 let main_src = inputs.write_file("main", b"new-main").expect("write");
334 let helper_src = inputs.write_file("helper", b"new-helper").expect("write");
335 let main_digest = super::hash_file(&main_src).unwrap();
336 let helper_digest = super::hash_file(&helper_src).unwrap();
337 if existing {
338 install
339 .write_file("bin/main", b"old-main")
340 .expect("old main");
341 install
342 .write_file("bin/helper", b"old-helper")
343 .expect("old helper");
344 } else {
345 fs::create_dir_all(install.path().join("bin")).unwrap();
346 }
347 let members = ArtifactSet::new(vec![
348 ArtifactMember::new(
349 MemberId::new("main").unwrap(),
350 inputs.path().join("main"),
351 "bin/main",
352 )
353 .unwrap()
354 .with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
355 ArtifactMember::new(
356 MemberId::new("helper").unwrap(),
357 inputs.path().join("helper"),
358 "bin/helper",
359 )
360 .unwrap()
361 .with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
362 ])
363 .unwrap();
364 let prepared = InstallPlan::new(
365 ProductId::new("bundle").unwrap(),
366 ReleaseId::new("r1").unwrap(),
367 install.path(),
368 members,
369 )
370 .unwrap()
371 .prepare()
372 .unwrap();
373 (inputs, install, prepared)
374 }
375
376 #[test]
377 fn commits_a_complete_multi_member_generation_and_releases_lock() {
378 let (_inputs, install, prepared) = prepared_bundle(true);
379 let verifier = allow_create();
380
381 let receipt =
382 commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).expect("commit");
383
384 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
385 assert_eq!(receipt.cleanup(), CleanupDisposition::Cleaned);
386 assert!(receipt.failure().is_none());
387 assert_eq!(
388 fs::read(install.path().join("bin/main")).unwrap(),
389 b"new-main"
390 );
391 assert_eq!(
392 fs::read(install.path().join("bin/helper")).unwrap(),
393 b"new-helper"
394 );
395 assert!(!install.path().join(".eggup-mutation.lock").exists());
396 assert!(install
397 .path()
398 .read_dir()
399 .unwrap()
400 .filter_map(std::result::Result::ok)
401 .all(|entry| !entry
402 .file_name()
403 .to_string_lossy()
404 .starts_with(".eggup-backup-")));
405 }
406
407 #[test]
408 fn post_commit_check_sees_complete_generation_and_holds_mutation_lock() {
409 let (_inputs, install, prepared) = prepared_bundle(true);
410 let verifier = allow_create();
411 let validated = prepared
412 .verify_integrity()
413 .unwrap()
414 .validate(&AllValidators::new())
415 .unwrap();
416 let receipt = validated
417 .commit_with_post_commit(
418 CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
419 PostCommitFailurePolicy::RollBack,
420 || {
421 assert_eq!(
422 fs::read(install.path().join("bin/main")).unwrap(),
423 b"new-main"
424 );
425 assert_eq!(
426 fs::read(install.path().join("bin/helper")).unwrap(),
427 b"new-helper"
428 );
429 assert!(MutationLock::acquire(
430 install.path(),
431 &ProductId::new("bundle").unwrap(),
432 &ReleaseId::new("r1").unwrap(),
433 )
434 .is_err());
435 Ok::<_, &str>(())
436 },
437 )
438 .unwrap();
439 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
440 assert!(receipt.post_commit_failure().is_none());
441 }
442
443 #[test]
444 fn post_commit_keep_installed_records_bounded_failure() {
445 let (_inputs, install, prepared) = prepared_bundle(true);
446 let verifier = allow_create();
447 let validated = prepared
448 .verify_integrity()
449 .unwrap()
450 .validate(&AllValidators::new())
451 .unwrap();
452 let receipt = validated
453 .commit_with_post_commit(
454 CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
455 PostCommitFailurePolicy::KeepInstalled,
456 || Err("é".repeat(400)),
457 )
458 .unwrap();
459 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
460 let failure = receipt.post_commit_failure().unwrap();
461 assert_eq!(failure.phase(), super::FailurePhase::PostCommit);
462 assert!(failure.detail().len() <= 512);
463 assert_eq!(
464 fs::read(install.path().join("bin/main")).unwrap(),
465 b"new-main"
466 );
467 assert_eq!(
468 fs::read(install.path().join("bin/helper")).unwrap(),
469 b"new-helper"
470 );
471 }
472
473 #[test]
474 fn post_commit_rollback_restores_old_generation_and_absent_members() {
475 let inputs = InstallationRoot::new().unwrap();
476 let install = InstallationRoot::new().unwrap();
477 let main = inputs.write_file("main", b"new-main").unwrap();
478 let helper = inputs.write_file("helper", b"new-helper").unwrap();
479 install.write_file("bin/main", b"old-main").unwrap();
480 let artifacts = ArtifactSet::new(vec![
481 ArtifactMember::new(MemberId::new("main").unwrap(), main, "bin/main")
482 .unwrap()
483 .with_integrity(super::IntegrityRequirement::Sha256(
484 super::hash_file(&inputs.path().join("main")).unwrap(),
485 )),
486 ArtifactMember::new(MemberId::new("helper").unwrap(), helper, "bin/helper")
487 .unwrap()
488 .with_integrity(super::IntegrityRequirement::Sha256(
489 super::hash_file(&inputs.path().join("helper")).unwrap(),
490 )),
491 ])
492 .unwrap();
493 let prepared = InstallPlan::new(
494 ProductId::new("bundle").unwrap(),
495 ReleaseId::new("r1").unwrap(),
496 install.path(),
497 artifacts,
498 )
499 .unwrap()
500 .prepare()
501 .unwrap();
502 let verifier = allow_create();
503 let receipt = prepared
504 .verify_integrity()
505 .unwrap()
506 .validate(&AllValidators::new())
507 .unwrap()
508 .commit_with_post_commit(
509 CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
510 PostCommitFailurePolicy::RollBack,
511 || Err("health check failed"),
512 )
513 .unwrap();
514 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
515 assert_eq!(
516 receipt.failure().unwrap().phase(),
517 super::FailurePhase::PostCommit
518 );
519 assert_eq!(
520 fs::read(install.path().join("bin/main")).unwrap(),
521 b"old-main"
522 );
523 assert!(!install.path().join("bin/helper").exists());
524 }
525
526 #[test]
527 fn post_commit_rollback_failure_retains_both_reports_and_evidence() {
528 let (_inputs, install, prepared) = prepared_bundle(true);
529 let verifier = allow_create();
530 let receipt = prepared
531 .verify_integrity()
532 .unwrap()
533 .validate(&AllValidators::new())
534 .unwrap()
535 .commit_with_post_commit_fault(
536 CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
537 PostCommitFailurePolicy::RollBack,
538 CommitFault::PostCommitRollback,
539 )
540 .unwrap();
541 assert_eq!(
542 receipt.disposition(),
543 TransactionDisposition::RecoveryRequired
544 );
545 assert_eq!(
546 receipt.post_commit_failure().unwrap().phase(),
547 super::FailurePhase::PostCommit
548 );
549 assert_eq!(
550 receipt.rollback_failure().unwrap().phase(),
551 super::FailurePhase::Rollback
552 );
553 assert!(receipt.recovery_path().unwrap().exists());
554 let recovery = receipt.recovery_path().unwrap().to_path_buf();
555 let _ = fs::remove_dir_all(recovery);
556 let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
557 }
558
559 #[test]
560 fn keep_installed_finalize_failure_preserves_both_failure_facts() {
561 let (_inputs, install, prepared) = prepared_bundle(true);
562 let verifier = allow_create();
563 let receipt = prepared
564 .verify_integrity()
565 .unwrap()
566 .validate(&AllValidators::new())
567 .unwrap()
568 .commit_with_post_commit_fault(
569 CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
570 PostCommitFailurePolicy::KeepInstalled,
571 CommitFault::Finalize,
572 )
573 .unwrap();
574 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
575 assert_eq!(
576 receipt.failure().unwrap().phase(),
577 super::FailurePhase::Finalize
578 );
579 assert_eq!(
580 receipt.post_commit_failure().unwrap().phase(),
581 super::FailurePhase::PostCommit
582 );
583 let recovery = receipt.recovery_path().unwrap().to_path_buf();
584 assert!(recovery.exists());
585 assert_eq!(
586 fs::read(install.path().join("bin/main")).unwrap(),
587 b"new-main"
588 );
589 let _ = fs::remove_dir_all(recovery);
590 let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
591 }
592
593 #[test]
594 fn post_commit_panic_is_reported_and_rolls_back() {
595 let (_inputs, install, prepared) = prepared_bundle(true);
596 let verifier = allow_create();
597 let receipt = prepared
598 .verify_integrity()
599 .unwrap()
600 .validate(&AllValidators::new())
601 .unwrap()
602 .commit_with_post_commit(
603 CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
604 PostCommitFailurePolicy::RollBack,
605 || -> Result<(), &str> { panic!("simulated check panic") },
606 )
607 .unwrap();
608 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
609 assert_eq!(
610 receipt.post_commit_failure().unwrap().detail(),
611 "post-commit check panicked"
612 );
613 assert_eq!(
614 fs::read(install.path().join("bin/main")).unwrap(),
615 b"old-main"
616 );
617 }
618
619 #[test]
620 fn partial_commit_failure_restores_every_old_member() {
621 let (_inputs, install, prepared) = prepared_bundle(true);
622 let verifier = allow_create();
623
624 let receipt = commit_verified_with_fault(
625 prepared,
626 &verifier,
627 AbsentPolicy::AllowCreate,
628 CommitFault::Commit(MemberId::new("helper").unwrap()),
629 )
630 .expect("receipt");
631
632 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
633 assert!(receipt.rollback_performed());
634 assert!(receipt.rollback_verified());
635 let failure = receipt.failure().expect("failure report");
636 assert_eq!(failure.phase(), super::FailurePhase::Commit);
637 assert_eq!(failure.member(), Some(&MemberId::new("helper").unwrap()));
638 assert_eq!(
639 fs::read(install.path().join("bin/main")).unwrap(),
640 b"old-main"
641 );
642 assert_eq!(
643 fs::read(install.path().join("bin/helper")).unwrap(),
644 b"old-helper"
645 );
646 }
647
648 #[test]
649 fn rollback_removes_new_members_that_were_absent_before_commit() {
650 let inputs = InstallationRoot::new().unwrap();
651 let install = InstallationRoot::new().unwrap();
652 let main_src = inputs.write_file("main", b"new-main").unwrap();
653 let helper_src = inputs.write_file("helper", b"new-helper").unwrap();
654 let main_digest = super::hash_file(&main_src).unwrap();
655 let helper_digest = super::hash_file(&helper_src).unwrap();
656 install.write_file("bin/main", b"old-main").unwrap();
657 let prepared = InstallPlan::new(
658 ProductId::new("bundle").unwrap(),
659 ReleaseId::new("r1").unwrap(),
660 install.path(),
661 ArtifactSet::new(vec![
662 ArtifactMember::new(
663 MemberId::new("main").unwrap(),
664 inputs.path().join("main"),
665 "bin/main",
666 )
667 .unwrap()
668 .with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
669 ArtifactMember::new(
670 MemberId::new("helper").unwrap(),
671 inputs.path().join("helper"),
672 "bin/helper",
673 )
674 .unwrap()
675 .with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
676 ])
677 .unwrap(),
678 )
679 .unwrap()
680 .prepare()
681 .unwrap();
682 let verifier = allow_create();
683
684 let receipt = commit_verified_with_fault(
685 prepared,
686 &verifier,
687 AbsentPolicy::AllowCreate,
688 CommitFault::Commit(MemberId::new("helper").unwrap()),
689 )
690 .unwrap();
691
692 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
693 assert_eq!(
694 fs::read(install.path().join("bin/main")).unwrap(),
695 b"old-main"
696 );
697 assert!(!install.path().join("bin/helper").exists());
698 }
699
700 #[test]
701 fn lock_contention_and_malformed_lock_fail_closed() {
702 let (_inputs, install, prepared) = prepared_bundle(false);
703 let product = ProductId::new("bundle").unwrap();
704 let release = ReleaseId::new("r1").unwrap();
705 let _lock = MutationLock::acquire(install.path(), &product, &release).unwrap();
706 let verifier = allow_create();
707
708 let error = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap_err();
709 assert!(matches!(error, Error::UpdateInProgress { .. }));
710 drop(_lock);
711
712 let (_inputs, install2, prepared) = prepared_bundle(false);
713 let lock = install2.path().join(".eggup-mutation.lock");
714 fs::write(&lock, b"malformed").unwrap();
715 let error = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap_err();
716 assert!(matches!(error, Error::UpdateInProgress { .. }));
717 assert!(lock.exists());
718 }
719
720 #[test]
721 fn rollback_failure_returns_recovery_required_and_retains_evidence() {
722 let (_inputs, install, prepared) = prepared_bundle(true);
723 let verifier = allow_create();
724
725 let receipt = commit_verified_with_fault(
726 prepared,
727 &verifier,
728 AbsentPolicy::AllowCreate,
729 CommitFault::CommitThenRollback(
730 MemberId::new("helper").unwrap(),
731 MemberId::new("main").unwrap(),
732 ),
733 )
734 .unwrap();
735
736 assert_eq!(
737 receipt.disposition(),
738 TransactionDisposition::RecoveryRequired
739 );
740 assert!(!receipt.rollback_verified());
741 assert!(receipt.recovery_path().is_some());
742 assert!(receipt.failure().is_some());
743 assert!(receipt.rollback_failure().is_some());
744 assert_eq!(
745 receipt.failure().unwrap().phase(),
746 super::FailurePhase::Commit
747 );
748 assert_eq!(
749 receipt.rollback_failure().unwrap().phase(),
750 super::FailurePhase::Rollback
751 );
752 assert!(install.path().join(".eggup-mutation.lock").exists());
753 let recovery = receipt.recovery_path().unwrap();
754 assert!(recovery.exists());
755 let _ = fs::remove_dir_all(recovery);
756 let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
757 }
758
759 #[test]
760 fn precommit_and_backup_failures_restore_old_state() {
761 let (_inputs, install, prepared) = prepared_bundle(true);
762 let verifier = allow_create();
763 let receipt = commit_verified_with_fault(
764 prepared,
765 &verifier,
766 AbsentPolicy::AllowCreate,
767 CommitFault::BeforeFirstCommit,
768 )
769 .unwrap();
770 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
771 assert!(receipt.failure().is_some());
772 assert_eq!(
773 fs::read(install.path().join("bin/main")).unwrap(),
774 b"old-main"
775 );
776
777 let (_inputs, install, prepared) = prepared_bundle(true);
778 let receipt = commit_verified_with_fault(
779 prepared,
780 &verifier,
781 AbsentPolicy::AllowCreate,
782 CommitFault::Backup(MemberId::new("helper").unwrap()),
783 )
784 .unwrap();
785 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
786 assert_eq!(
787 receipt.failure().unwrap().phase(),
788 super::FailurePhase::Backup
789 );
790 assert_eq!(
791 fs::read(install.path().join("bin/helper")).unwrap(),
792 b"old-helper"
793 );
794 }
795
796 #[test]
797 fn injected_lock_creation_failure_performs_no_mutation() {
798 let (_inputs, install, prepared) = prepared_bundle(true);
799 let verifier = allow_create();
800
801 let error = commit_verified_with_fault(
802 prepared,
803 &verifier,
804 AbsentPolicy::AllowCreate,
805 CommitFault::LockCreation,
806 )
807 .unwrap_err();
808
809 assert!(matches!(error, Error::InvalidInput(_)));
810 assert_eq!(
811 fs::read(install.path().join("bin/main")).unwrap(),
812 b"old-main"
813 );
814 assert!(!install.path().join(".eggup-mutation.lock").exists());
815 }
816
817 #[cfg(unix)]
818 #[test]
819 fn destination_links_fail_immediately_before_backup() {
820 use std::fs::hard_link;
821 use std::os::unix::fs::symlink;
822
823 let inputs = InstallationRoot::new().unwrap();
824 let install = InstallationRoot::new().unwrap();
825 let src = inputs.write_file("main", b"new-main").unwrap();
826 let digest = super::hash_file(&src).unwrap();
827 install.write_file("bin/real", b"old-main").unwrap();
828 symlink(install.path().join("real"), install.path().join("bin/main")).unwrap();
829 let prepared = InstallPlan::new(
830 ProductId::new("bundle").unwrap(),
831 ReleaseId::new("r1").unwrap(),
832 install.path(),
833 ArtifactSet::single(
834 ArtifactMember::new(
835 MemberId::new("main").unwrap(),
836 inputs.path().join("main"),
837 "bin/main",
838 )
839 .unwrap()
840 .with_integrity(super::IntegrityRequirement::Sha256(digest)),
841 )
842 .unwrap(),
843 )
844 .unwrap()
845 .prepare()
846 .unwrap();
847 let verifier = allow_create();
848 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
849 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
850 assert_eq!(
851 receipt.failure().unwrap().phase(),
852 super::FailurePhase::Ownership
853 );
854 assert_eq!(
855 fs::read(install.path().join("bin/real")).unwrap(),
856 b"old-main"
857 );
858
859 let (_inputs, install, prepared) = prepared_bundle(true);
860 hard_link(
861 install.path().join("bin/main"),
862 install.path().join("bin/linked"),
863 )
864 .unwrap();
865 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
866 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
867 assert_eq!(
868 fs::read(install.path().join("bin/main")).unwrap(),
869 b"old-main"
870 );
871 }
872
873 #[test]
874 fn hashes_known_bytes_and_parses_strict_sidecars() {
875 let fixture = InstallationRoot::new().unwrap();
876 let path = fixture.write_file("abc", b"abc").unwrap();
877 let manifest = super::parse_sha256_sidecar(
878 "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD abc\n",
879 )
880 .unwrap();
881 assert_eq!(manifest.filename(), Some("abc"));
882 assert_eq!(
883 super::verify_file(&path, &manifest).unwrap(),
884 *manifest.digest()
885 );
886 assert!(super::parse_sha256_sidecar("abc\ndef").is_err());
887 assert!(super::parse_sha256_sidecar(
888 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad other"
889 )
890 .and_then(|manifest| super::verify_file(&path, &manifest))
891 .is_err());
892 assert!(super::parse_sha256_sidecar("not-a-digest abc").is_err());
893 }
894
895 #[test]
896 fn integrity_verification_precedes_candidate_validation() {
897 let inputs = InstallationRoot::new().unwrap();
898 let install = InstallationRoot::new().unwrap();
899 let source = inputs.write_file("main", b"verified bytes").unwrap();
900 let digest = super::hash_file(&source).unwrap();
901 let member = ArtifactMember::new(MemberId::new("main").unwrap(), source, "bin/main")
902 .unwrap()
903 .with_integrity(super::IntegrityRequirement::Sha256(digest));
904 let verified = InstallPlan::new(
905 ProductId::new("eggup").unwrap(),
906 ReleaseId::new("r1").unwrap(),
907 install.path(),
908 ArtifactSet::single(member).unwrap(),
909 )
910 .unwrap()
911 .prepare()
912 .unwrap()
913 .verify_integrity()
914 .unwrap();
915 assert_eq!(
916 verified
917 .integrity(&MemberId::new("main").unwrap())
918 .unwrap()
919 .status(),
920 super::IntegrityStatus::Verified
921 );
922
923 let bad_source = inputs.write_file("bad", b"bad bytes").unwrap();
924 let bad_member = ArtifactMember::new(MemberId::new("bad").unwrap(), bad_source, "bin/bad")
925 .unwrap()
926 .with_integrity(super::IntegrityRequirement::Sha256(digest));
927 assert!(matches!(
928 InstallPlan::new(
929 ProductId::new("eggup").unwrap(),
930 ReleaseId::new("r1").unwrap(),
931 install.path(),
932 ArtifactSet::single(bad_member).unwrap(),
933 )
934 .unwrap()
935 .prepare()
936 .unwrap()
937 .verify_integrity(),
938 Err(Error::VerificationFailed(_))
939 ));
940
941 let no_requirement = ArtifactMember::new(
942 MemberId::new("none").unwrap(),
943 inputs.write_file("none", b"no declaration").unwrap(),
944 "bin/none",
945 )
946 .unwrap();
947 let verified = InstallPlan::new(
948 ProductId::new("eggup").unwrap(),
949 ReleaseId::new("r1").unwrap(),
950 install.path(),
951 ArtifactSet::single(no_requirement).unwrap(),
952 )
953 .unwrap()
954 .prepare()
955 .unwrap()
956 .verify_integrity()
957 .unwrap();
958 let validator = ExactIdentityValidator::new(MemberId::new("none").unwrap(), "anything");
959 assert!(matches!(
960 verified.validate(&validator),
961 Err(Error::VerificationFailed(_))
962 ));
963 }
964
965 #[cfg(unix)]
966 #[test]
967 fn bounded_candidates_are_exact_and_environment_is_cleared() {
968 use std::os::unix::fs::PermissionsExt;
969
970 let inputs = InstallationRoot::new().unwrap();
971 let install = InstallationRoot::new().unwrap();
972 let script = inputs
973 .write_file("main", b"#!/bin/sh\nprintf 'eggup 1.2.3\\n'\n")
974 .unwrap();
975 fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
976 let digest = super::hash_file(&script).unwrap();
977 let member = ArtifactMember::new(MemberId::new("main").unwrap(), script, "bin/main")
978 .unwrap()
979 .with_integrity(super::IntegrityRequirement::Sha256(digest));
980 fs::create_dir_all(install.path().join("bin")).unwrap();
981 let verified = InstallPlan::new(
982 ProductId::new("eggup").unwrap(),
983 ReleaseId::new("r1").unwrap(),
984 install.path(),
985 ArtifactSet::single(member).unwrap(),
986 )
987 .unwrap()
988 .prepare()
989 .unwrap()
990 .verify_integrity()
991 .unwrap();
992 let validator =
993 ExactIdentityValidator::new(MemberId::new("main").unwrap(), "eggup 1.2.3\n");
994 let validated = verified.validate(&validator).unwrap();
995 let verifier = allow_create();
996 assert_eq!(
997 validated
998 .commit(CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate))
999 .unwrap()
1000 .disposition(),
1001 TransactionDisposition::Committed
1002 );
1003
1004 let secret_script = inputs
1005 .write_file(
1006 "secret",
1007 b"#!/bin/sh\nprintf '%s' \"${EGGUP_SECRET-unset}\"\n",
1008 )
1009 .unwrap();
1010 fs::set_permissions(&secret_script, fs::Permissions::from_mode(0o755)).unwrap();
1011 let output = super::run_bounded(&super::CommandSpec::new(secret_script)).unwrap();
1012 assert!(output.success(), "bounded candidate output: {output:?}");
1013 assert_eq!(output.stdout(), b"unset");
1014 }
1015
1016 #[cfg(unix)]
1017 #[test]
1018 fn bounded_runner_kills_timeouts_and_limits_output() {
1019 use std::os::unix::fs::PermissionsExt;
1020
1021 let fixture = InstallationRoot::new().unwrap();
1022 let timeout_script = fixture
1023 .write_file("timeout", b"#!/bin/sh\nsleep 2\n")
1024 .unwrap();
1025 let noisy_script = fixture
1026 .write_file("noisy", b"#!/bin/sh\nprintf '1234567890'\n")
1027 .unwrap();
1028 for path in [&timeout_script, &noisy_script] {
1029 fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
1030 }
1031 let timed_out = super::run_bounded(
1032 &super::CommandSpec::new(timeout_script).timeout(Duration::from_millis(20)),
1033 )
1034 .unwrap();
1035 assert!(timed_out.timed_out());
1036 assert!(!timed_out.success());
1037 let limited =
1038 super::run_bounded(&super::CommandSpec::new(noisy_script).max_output_bytes(4)).unwrap();
1039 assert!(limited.output_limited());
1040 assert!(!limited.success());
1041 }
1042
1043 #[cfg(unix)]
1044 #[test]
1045 fn cross_member_identity_requires_bundle_agreement() {
1046 use std::os::unix::fs::PermissionsExt;
1047
1048 let inputs = InstallationRoot::new().unwrap();
1049 let install = InstallationRoot::new().unwrap();
1050 let main = inputs
1051 .write_file("main", b"#!/bin/sh\nprintf 'bundle 9\\n'\n")
1052 .unwrap();
1053 let helper = inputs
1054 .write_file("helper", b"#!/bin/sh\nprintf 'bundle 9\\n'\n")
1055 .unwrap();
1056 for path in [&main, &helper] {
1057 fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
1058 }
1059 let main_digest = super::hash_file(&main).unwrap();
1060 let helper_digest = super::hash_file(&helper).unwrap();
1061 let verified = InstallPlan::new(
1062 ProductId::new("bundle").unwrap(),
1063 ReleaseId::new("r9").unwrap(),
1064 install.path(),
1065 ArtifactSet::new(vec![
1066 ArtifactMember::new(MemberId::new("main").unwrap(), main, "bin/main")
1067 .unwrap()
1068 .with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
1069 ArtifactMember::new(MemberId::new("helper").unwrap(), helper, "bin/helper")
1070 .unwrap()
1071 .with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
1072 ])
1073 .unwrap(),
1074 )
1075 .unwrap()
1076 .prepare()
1077 .unwrap()
1078 .verify_integrity()
1079 .unwrap();
1080 let validator = super::CrossMemberAgreementValidator::new(
1081 [
1082 MemberId::new("main").unwrap(),
1083 MemberId::new("helper").unwrap(),
1084 ],
1085 "bundle 9\n",
1086 );
1087 assert!(verified.validate(&validator).is_ok());
1088 }
1089
1090 fn single_verified(
1093 new_bytes: &[u8],
1094 old_bytes: Option<&[u8]>,
1095 with_bin_dir: bool,
1096 ) -> (
1097 InstallationRoot,
1098 InstallationRoot,
1099 super::PreparedTransaction,
1100 [u8; 32],
1101 ) {
1102 let inputs = InstallationRoot::new().unwrap();
1103 let install = InstallationRoot::new().unwrap();
1104 let src = inputs.write_file("main", new_bytes).unwrap();
1105 let digest = super::hash_file(&src).unwrap();
1106 if let Some(old) = old_bytes {
1107 install.write_file("bin/main", old).unwrap();
1108 } else if with_bin_dir {
1109 fs::create_dir_all(install.path().join("bin")).unwrap();
1110 }
1111 let member = ArtifactMember::new(MemberId::new("main").unwrap(), src, "bin/main")
1112 .unwrap()
1113 .with_integrity(super::IntegrityRequirement::Sha256(digest));
1114 let prepared = InstallPlan::new(
1115 ProductId::new("p").unwrap(),
1116 ReleaseId::new("r1").unwrap(),
1117 install.path(),
1118 ArtifactSet::single(member).unwrap(),
1119 )
1120 .unwrap()
1121 .prepare()
1122 .unwrap();
1123 (inputs, install, prepared, digest)
1124 }
1125
1126 #[test]
1127 fn ownership_owned_allows_replacement_and_reports_no_failure() {
1128 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1129 let old_digest = super::hash_file(&install.path().join("bin/main")).unwrap();
1130 let verifier = ExactDigestVerifier::new(vec![(MemberId::new("main").unwrap(), old_digest)]);
1131 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
1132 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1133 assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"new");
1134 }
1135
1136 #[test]
1137 fn ownership_foreign_and_unknown_fail_closed_with_zero_mutation() {
1138 for fixed in [Ownership::Foreign, Ownership::Unknown] {
1139 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1140 let verifier = FixedVerifier(fixed);
1141 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1142 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1143 assert_eq!(
1144 receipt.failure().unwrap().phase(),
1145 super::FailurePhase::Ownership
1146 );
1147 assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
1148 }
1149 }
1150
1151 #[test]
1152 fn ownership_absent_create_policy_is_enforced() {
1153 let (_i, install, prepared, _) = single_verified(b"new", None, true);
1154 let verifier = AbsentOnlyVerifier;
1155 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1156 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1157 assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"new");
1158
1159 let (_i, install, prepared, _) = single_verified(b"new", None, true);
1160 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
1161 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1162 assert!(!install.path().join("bin/main").exists());
1163 }
1164
1165 #[test]
1166 fn ownership_flapping_between_preflight_and_lock_fails() {
1167 use std::cell::Cell;
1168 #[derive(Debug)]
1169 struct Flap {
1170 first: Ownership,
1171 second: Ownership,
1172 calls: Cell<usize>,
1173 }
1174 impl OwnershipVerifier for Flap {
1175 fn verify(&self, _m: &MemberId, _d: &Path) -> Ownership {
1176 let n = self.calls.get();
1177 self.calls.set(n + 1);
1178 if n == 0 {
1179 self.first
1180 } else {
1181 self.second
1182 }
1183 }
1184 }
1185 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1186 let verifier = Flap {
1187 first: Ownership::Owned,
1188 second: Ownership::Foreign,
1189 calls: Cell::new(0),
1190 };
1191 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1192 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1193 assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
1194 }
1195
1196 #[test]
1197 fn missing_parent_fails_without_creating_directories() {
1198 let (_i, install, prepared, _) = single_verified(b"new", None, false);
1199 assert!(!install.path().join("bin").exists());
1200 let verifier = allow_create();
1201 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1202 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1203 assert!(!install.path().join("bin").exists());
1204 assert!(!install.path().join("bin/main").exists());
1205 }
1206
1207 #[cfg(unix)]
1208 #[test]
1209 fn symlinked_parent_fails_closed() {
1210 use std::os::unix::fs::symlink;
1211 let (_i, install, prepared, _) = single_verified(b"new", None, false);
1212 fs::create_dir_all(install.path().join("real-bin")).unwrap();
1213 symlink(install.path().join("real-bin"), install.path().join("bin")).unwrap();
1214 let verifier = allow_create();
1216 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1217 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1218 }
1219
1220 #[test]
1221 fn staged_mutation_after_validation_fails_before_live_mutation() {
1222 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1223 let verified = prepared.verify_integrity().unwrap();
1224 let validated = verified.validate(&AllValidators::new()).unwrap();
1225 let staged = validated
1227 .staged_path(&MemberId::new("main").unwrap())
1228 .unwrap();
1229 fs::write(&staged, b"tampered").unwrap();
1230 let verifier = allow_create();
1231 let receipt = validated
1232 .commit(CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate))
1233 .unwrap();
1234 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1235 assert_eq!(
1236 receipt.failure().unwrap().phase(),
1237 super::FailurePhase::StageRevalidation
1238 );
1239 assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
1240 }
1241
1242 #[test]
1243 fn unverified_integrity_none_cannot_commit() {
1244 let inputs = InstallationRoot::new().unwrap();
1245 let install = InstallationRoot::new().unwrap();
1246 fs::create_dir_all(install.path().join("bin")).unwrap();
1247 let src = inputs.write_file("main", b"bytes").unwrap();
1248 let member = ArtifactMember::new(MemberId::new("main").unwrap(), src, "bin/main").unwrap();
1249 let prepared = InstallPlan::new(
1250 ProductId::new("p").unwrap(),
1251 ReleaseId::new("r1").unwrap(),
1252 install.path(),
1253 ArtifactSet::single(member).unwrap(),
1254 )
1255 .unwrap()
1256 .prepare()
1257 .unwrap();
1258 let verified = prepared.verify_integrity().unwrap();
1259 assert!(verified.validate(&AllValidators::new()).is_err());
1260 }
1261
1262 #[test]
1263 fn cleanup_failure_reports_real_backup_root() {
1264 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1265 let verifier = allow_create();
1266 let receipt = commit_verified_with_fault(
1268 prepared,
1269 &verifier,
1270 AbsentPolicy::AllowCreate,
1271 CommitFault::Finalize,
1272 )
1273 .unwrap();
1274 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1275 assert_eq!(receipt.cleanup(), CleanupDisposition::RetainedForRecovery);
1276 let path = receipt.recovery_path().unwrap();
1277 assert!(path.exists());
1278 assert!(!path.to_string_lossy().contains("cleanup-failed"));
1279 assert_eq!(
1280 receipt.failure().unwrap().phase(),
1281 super::FailurePhase::Finalize
1282 );
1283 let _ = fs::remove_dir_all(path);
1284 let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
1285 }
1286
1287 #[cfg(unix)]
1288 #[test]
1289 fn transaction_owned_state_is_owner_private() {
1290 use std::os::unix::fs::PermissionsExt;
1291 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1292 let stage_mode = fs::metadata(prepared.stage_root())
1293 .unwrap()
1294 .permissions()
1295 .mode()
1296 & 0o777;
1297 assert_eq!(stage_mode, 0o700);
1298 let staged = prepared
1299 .staged_path(&MemberId::new("main").unwrap())
1300 .unwrap();
1301 let file_mode = fs::metadata(&staged).unwrap().permissions().mode() & 0o777;
1302 assert_eq!(file_mode, 0o600);
1303 let verifier = allow_create();
1304 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1305 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1306 let (_i2, install2, prepared2, _) = single_verified(b"n2", Some(b"o2"), false);
1308 let receipt = commit_verified_with_fault(
1309 prepared2,
1310 &verifier,
1311 AbsentPolicy::AllowCreate,
1312 CommitFault::Finalize,
1313 )
1314 .unwrap();
1315 let lock_mode = fs::metadata(install2.path().join(".eggup-mutation.lock"))
1316 .unwrap()
1317 .permissions()
1318 .mode()
1319 & 0o777;
1320 assert_eq!(lock_mode, 0o600);
1321 let _ = fs::remove_dir_all(receipt.recovery_path().unwrap());
1322 let _ = fs::remove_file(install2.path().join(".eggup-mutation.lock"));
1323 let _ = install;
1324 }
1325
1326 #[test]
1327 fn lock_inspect_never_deletes_and_reports_status() {
1328 let install = InstallationRoot::new().unwrap();
1329 assert_eq!(
1330 MutationLock::inspect(install.path()).unwrap(),
1331 super::LockStatus::Available
1332 );
1333 let product = ProductId::new("p").unwrap();
1334 let release = ReleaseId::new("r").unwrap();
1335 let lock = MutationLock::acquire(install.path(), &product, &release).unwrap();
1336 match MutationLock::inspect(install.path()).unwrap() {
1337 super::LockStatus::Held { contents, .. } => assert!(contents.is_some()),
1338 other => panic!("expected held, got {other:?}"),
1339 }
1340 drop(lock);
1341 assert_eq!(
1343 MutationLock::inspect(install.path()).unwrap(),
1344 super::LockStatus::Available
1345 );
1346 fs::write(install.path().join(".eggup-mutation.lock"), b"malformed").unwrap();
1347 match MutationLock::inspect(install.path()).unwrap() {
1348 super::LockStatus::Held { .. } | super::LockStatus::Malformed { .. } => {}
1349 other => panic!("expected held/malformed, got {other:?}"),
1350 }
1351 assert!(install.path().join(".eggup-mutation.lock").exists());
1353 }
1354
1355 #[test]
1356 fn oversized_lock_record_is_malformed_not_deleted() {
1357 let install = InstallationRoot::new().unwrap();
1358 let big = vec![b'x'; 8192];
1359 fs::write(install.path().join(".eggup-mutation.lock"), &big).unwrap();
1360 match MutationLock::inspect(install.path()).unwrap() {
1361 super::LockStatus::Malformed { .. } => {}
1362 other => panic!("expected malformed, got {other:?}"),
1363 }
1364 assert!(install.path().join(".eggup-mutation.lock").exists());
1365 }
1366
1367 #[test]
1368 fn exact_digest_verifier_proves_ownership() {
1369 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1370 let live_digest = super::hash_file(&install.path().join("bin/main")).unwrap();
1371 let verifier =
1372 ExactDigestVerifier::new(vec![(MemberId::new("main").unwrap(), live_digest)]);
1373 assert_eq!(
1374 verifier.verify(
1375 &MemberId::new("main").unwrap(),
1376 &install.path().join("bin/main")
1377 ),
1378 Ownership::Owned
1379 );
1380 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
1381 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1382 }
1383
1384 #[test]
1385 fn absent_only_verifier_never_authorizes_replacement() {
1386 let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1387 let verifier = AbsentOnlyVerifier;
1388 assert_eq!(
1389 verifier.verify(
1390 &MemberId::new("main").unwrap(),
1391 &install.path().join("bin/main")
1392 ),
1393 Ownership::Foreign
1394 );
1395 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1396 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1397 }
1398
1399 #[test]
1400 fn failure_reports_carry_phase_category_and_member() {
1401 let (_i, _install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1402 let verifier = FixedVerifier(Ownership::Foreign);
1403 let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1404 let failure = receipt.failure().unwrap();
1405 assert_eq!(failure.phase(), super::FailurePhase::Ownership);
1406 assert!(!failure.detail().is_empty());
1407 assert!(failure.detail().len() <= 512);
1408 }
1409
1410 #[test]
1411 fn checksum_only_state_is_explicit_in_docs() {
1412 let desc = env!("CARGO_PKG_DESCRIPTION");
1418 let _ = desc;
1419 }
1420
1421 #[allow(dead_code)]
1422 fn _unused() {
1423 let _m: HashMap<MemberId, [u8; 32]> = HashMap::new();
1424 }
1425}