Add explicit vocabulary and named operation contexts

This commit is contained in:
2026-10-29 22:41:38 +02:00
parent ede45d224a
commit 8413b382b1
14 changed files with 344 additions and 241 deletions
+107 -44
View File
@@ -287,17 +287,30 @@ pub trait OperationExecutor: Send + 'static {
}
}
enum DeliveryMode {
FirstIssue,
Reissue,
}
struct PreparedDestinationContext {
identity: PublicKey,
include_qr: bool,
mode: DeliveryMode,
attempt: FirstIssueAttempt,
generation: u64,
destination: PreparedDestination,
}
struct RestorePreviewContext {
_archive_name: String,
generation: u64,
preview: RestorePreview,
}
#[derive(Default)]
pub struct ApplicationExecutor {
prepared_destination: Option<(
PublicKey,
bool,
bool,
FirstIssueAttempt,
u64,
PreparedDestination,
)>,
restore_preview: Option<(String, u64, RestorePreview)>,
prepared_destination: Option<PreparedDestinationContext>,
restore_preview: Option<RestorePreviewContext>,
}
impl OperationExecutor for ApplicationExecutor {
@@ -379,14 +392,18 @@ impl OperationExecutor for ApplicationExecutor {
attempt,
prepared_generation: generation,
};
self.prepared_destination = Some((
self.prepared_destination = Some(PreparedDestinationContext {
identity,
include_qr,
reissue,
mode: if reissue {
DeliveryMode::Reissue
} else {
DeliveryMode::FirstIssue
},
attempt,
generation,
destination,
));
});
CommandResult::DeliveryDestination(summary)
}),
UiCommand::DeliverFirstIssue {
@@ -394,23 +411,16 @@ impl OperationExecutor for ApplicationExecutor {
overwrite_confirmed,
prepared_generation,
} => {
let (
identity,
include_qr,
reissue,
prepared_attempt,
destination_generation,
mut destination,
) = self
let prepared = self
.prepared_destination
.take()
.ok_or_else(|| OperationError {
message: "No prepared output destination is available.".to_owned(),
})?;
if reissue
|| prepared_attempt != attempt
if !matches!(prepared.mode, DeliveryMode::FirstIssue)
|| prepared.attempt != attempt
|| !destination_generation_matches(
destination_generation,
prepared.generation,
prepared_generation,
generation,
)
@@ -420,6 +430,9 @@ impl OperationExecutor for ApplicationExecutor {
.to_owned(),
});
}
let identity = prepared.identity;
let include_qr = prepared.include_qr;
let mut destination = prepared.destination;
if destination.overwrite_confirmation_required() {
if !overwrite_confirmed {
return Err(OperationError {
@@ -519,7 +532,11 @@ impl OperationExecutor for ApplicationExecutor {
changes,
preview_generation: generation,
};
self.restore_preview = Some((file_name, generation, preview));
self.restore_preview = Some(RestorePreviewContext {
_archive_name: file_name,
generation,
preview,
});
CommandResult::RestorePreview(summary)
})
}
@@ -532,14 +549,12 @@ impl OperationExecutor for ApplicationExecutor {
Ok(CommandResult::Cancelled)
}
UiCommand::ConfirmRestore { preview_generation } => {
let (_, stored_generation, preview) =
self.restore_preview.take().ok_or_else(|| OperationError {
message:
"Restore preview is no longer available; select the archive again."
.to_owned(),
})?;
let context = self.restore_preview.take().ok_or_else(|| OperationError {
message: "Restore preview is no longer available; select the archive again."
.to_owned(),
})?;
if !restore_preview_generation_matches(
stored_generation,
context.generation,
preview_generation,
generation,
) {
@@ -547,7 +562,8 @@ impl OperationExecutor for ApplicationExecutor {
message: "Restore preview is stale; select the archive again.".to_owned(),
});
}
app.confirm_restore(preview).map(CommandResult::Restored)
app.confirm_restore(context.preview)
.map(CommandResult::Restored)
}
};
result.map_err(|error| OperationError {
@@ -573,28 +589,23 @@ impl OperationExecutor for ApplicationExecutor {
overwrite_confirmed,
prepared_generation,
} => {
let (
prepared_identity,
_include_qr,
reissue,
_attempt,
destination_generation,
mut destination,
) = self
let prepared = self
.prepared_destination
.take()
.ok_or_else(|| OperationError {
message: "No prepared output destination is available.".to_owned(),
})?;
if prepared_identity != identity
|| !reissue
|| destination_generation != prepared_generation
if prepared.identity != identity
|| !matches!(prepared.mode, DeliveryMode::Reissue)
|| prepared.generation != prepared_generation
{
return Err(OperationError {
message: "Prepared output destination is stale; select it again."
.to_owned(),
});
}
let include_qr = prepared.include_qr;
let mut destination = prepared.destination;
if destination.overwrite_confirmation_required() {
if !overwrite_confirmed {
return Err(OperationError {
@@ -604,7 +615,7 @@ impl OperationExecutor for ApplicationExecutor {
destination = destination.confirm_overwrite().map_err(operation_error)?;
}
let delivery = app
.reissue_client_file(identity, password.expose(), destination, _include_qr)
.reissue_client_file(identity, password.expose(), destination, include_qr)
.map_err(|error| OperationError {
message: error.to_string(),
})?;
@@ -1088,6 +1099,58 @@ mod tests {
assert!(!restore_preview_generation_matches(8, 8, 8));
}
#[test]
fn prepared_delivery_is_cleared_by_cancel_and_unrelated_commands() {
let (_root, mut app) = application();
let identity = PublicKey::from_bytes([44; 32]);
let mut executor = ApplicationExecutor::default();
let path = _root.path().join("prepared.conf");
let prepare = || UiCommand::PrepareDelivery {
identity,
path: path.clone(),
include_qr: false,
reissue: false,
attempt: FirstIssueAttempt::Initial,
};
let deliver = || UiCommand::DeliverFirstIssue {
attempt: FirstIssueAttempt::Initial,
overwrite_confirmed: false,
prepared_generation: 7,
};
assert!(matches!(
executor.execute_at(&mut app, prepare(), 7),
Ok(CommandResult::DeliveryDestination(_))
));
assert!(matches!(
executor.execute_at(&mut app, UiCommand::CancelPreparedDelivery, 7),
Ok(CommandResult::Cancelled)
));
assert_eq!(
executor
.execute_at(&mut app, deliver(), 7)
.unwrap_err()
.message,
"No prepared output destination is available."
);
assert!(matches!(
executor.execute_at(&mut app, prepare(), 7),
Ok(CommandResult::DeliveryDestination(_))
));
assert!(matches!(
executor.execute_at(&mut app, UiCommand::ListBackups, 7),
Ok(CommandResult::Backups(_))
));
assert_eq!(
executor
.execute_at(&mut app, deliver(), 7)
.unwrap_err()
.message,
"No prepared output destination is available."
);
}
struct FakeStatistics {
started: Option<mpsc::SyncSender<()>>,
release: Option<Receiver<()>>,