Implement audited findings
Some checks failed
Dependency security audit / rustsec (push) Failing after 3s

This commit is contained in:
2026-08-27 15:34:31 +02:00
parent 2feecae70d
commit c2dd826920
11 changed files with 773 additions and 220 deletions

View File

@@ -60,6 +60,15 @@ impl TreeCommitter for Committer {
}
}
struct SabotagingCommitter(Box<dyn FnMut()>);
impl TreeCommitter for SabotagingCommitter {
fn commit(&mut self, _change: &TreeCommit) -> Result<(), TreeCommitError> {
(self.0)();
Err(TreeCommitError::new("simulated operation failure"))
}
}
#[test]
fn remove_entry_supports_confirmation_force_and_empty_cleanup() -> TestResult {
let fixture = FixtureSet::load()?;
@@ -514,6 +523,97 @@ fn collisions_auxiliary_files_and_commit_failure_never_leave_partial_trees() ->
Ok(())
}
#[test]
fn rollback_failures_from_sources_destinations_and_cleanup_retain_the_operation() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let mut provider = Secrets::all(&fixture);
let source_collision = store.path().join("email/personal.gpg");
let mut committer = SabotagingCommitter(Box::new(move || {
fs::create_dir(&source_collision).expect("block source restoration");
}));
let error = TreeMutator::new(&repository, &keys)
.move_tree(
&MoveRequest {
source: "email/personal".into(),
destination: "archive/personal".into(),
force: false,
},
OverwriteDecision::Allow,
None,
&mut provider,
&mut committer,
)
.expect_err("source restoration must fail");
assert_operation_and_rollback(error);
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let mut provider = Secrets::all(&fixture);
let destination = store.path().join("archive/personal.gpg");
let mut committer = SabotagingCommitter(Box::new(move || {
fs::remove_file(&destination).expect("remove destination entry");
fs::create_dir(&destination).expect("block destination restoration");
}));
let error = TreeMutator::new(&repository, &keys)
.copy(
&CopyRequest {
source: "email/personal".into(),
destination: "archive/personal".into(),
force: false,
},
OverwriteDecision::Allow,
None,
&mut provider,
&mut committer,
)
.expect_err("destination restoration must fail");
assert_operation_and_rollback(error);
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let mut provider = Secrets::all(&fixture);
let blocker = store.path().join("archive/blocker");
let mut committer = SabotagingCommitter(Box::new(move || {
fs::write(&blocker, b"concurrent file").expect("block directory cleanup");
}));
let error = TreeMutator::new(&repository, &keys)
.copy(
&CopyRequest {
source: "email/personal".into(),
destination: "archive/personal".into(),
force: false,
},
OverwriteDecision::Allow,
None,
&mut provider,
&mut committer,
)
.expect_err("created-directory cleanup must fail");
assert_operation_and_rollback(error);
Ok(())
}
fn assert_operation_and_rollback(error: MutationError) {
let MutationError::RollbackFailed {
operation,
rollback,
} = error
else {
panic!("operation and rollback errors must both be retained");
};
assert!(matches!(*operation, MutationError::Commit(_)));
assert!(matches!(
*rollback,
MutationError::Repository(_) | MutationError::TreeChanged { .. }
));
}
fn remove(entry: &str, recursive: bool, force: bool) -> RemoveRequest {
RemoveRequest {
entry: entry.into(),