use super::{ CommandResult, DeliveryDestinationSummary, FirstIssueAttempt, FirstIssueStatus, FirstIssueSummary, OperationError, OperationExecutor, QrStatus, ReissueSummary, RestartPreviewSummary, RestorePreviewSummary, SensitiveCommand, UiCommand, }; use crate::app::{Application, FirstIssueDeliveryOutcome, RestorePreview}; use crate::delivery::files::PreparedDestination; use crate::delivery::{QrCodeOutput, QrError}; use crate::domain::PublicKey; 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, restore_preview: Option, } impl OperationExecutor for ApplicationExecutor { fn execute( &mut self, app: &mut Application, command: UiCommand, ) -> Result { self.execute_at(app, command, 0) } fn execute_at( &mut self, app: &mut Application, command: UiCommand, generation: u64, ) -> Result { let delivery_continuation = matches!(command, UiCommand::DeliverFirstIssue { .. }); let restore_continuation = matches!( command, UiCommand::ConfirmRestore { .. } | UiCommand::CancelRestorePreview ); if !delivery_continuation && !matches!(&command, UiCommand::CancelPreparedDelivery) { self.prepared_destination = None; } if !restore_continuation && !matches!(&command, UiCommand::PreviewRestore { .. }) { self.restore_preview = None; } let result = match command { UiCommand::CreateClient { alias, manual_addresses, created_utc, } => app .prepare_new_client(&alias, manual_addresses, created_utc.as_deref()) .map(CommandResult::ClientCreated), UiCommand::RenameClient { identity, alias } => app .rename_client(identity, &alias) .map(|()| CommandResult::ClientRenamed), UiCommand::DeleteClient { identity } => app .delete_client(identity) .map(|record_removed| CommandResult::ClientDeleted { record_removed }), UiCommand::DiscardDraft => app.discard_draft().map(|()| CommandResult::DraftDiscarded), UiCommand::Commit => app.commit().map(CommandResult::Committed), UiCommand::CreateBackup => app.create_manual_backup().map(CommandResult::BackupCreated), UiCommand::ListBackups => app.list_backups().map(CommandResult::Backups), UiCommand::ImportSelectedFile { file_name } => app .import_selected_file(&file_name) .map(CommandResult::SelectedFileImported), UiCommand::PreviewRestart => app.preview_restart().map(|preview| { CommandResult::RestartPreview(RestartPreviewSummary { has_draft: preview.has_draft(), interface: preview.interface().to_owned(), systemd_unit: preview.systemd_unit().to_owned(), }) }), UiCommand::Restart(choice) => { let result = app .preview_restart() .map(|preview| app.restart(preview, choice)); result.map(CommandResult::Restarted) } UiCommand::PrepareDelivery { identity, path, include_qr, reissue, attempt, } => app .prepare_first_issue_destination(path) .map_err(crate::app::AppError::FileOutput) .map(|destination| { let summary = DeliveryDestinationSummary { identity, path: destination.path().to_path_buf(), overwrite_required: destination.overwrite_confirmation_required(), include_qr, reissue, attempt, prepared_generation: generation, }; self.prepared_destination = Some(PreparedDestinationContext { identity, include_qr, mode: if reissue { DeliveryMode::Reissue } else { DeliveryMode::FirstIssue }, attempt, generation, destination, }); CommandResult::DeliveryDestination(summary) }), UiCommand::DeliverFirstIssue { attempt, overwrite_confirmed, prepared_generation, } => { let prepared = self .prepared_destination .take() .ok_or_else(|| OperationError { message: "No prepared output destination is available.".to_owned(), })?; if !matches!(prepared.mode, DeliveryMode::FirstIssue) || prepared.attempt != attempt || !destination_generation_matches( prepared.generation, prepared_generation, generation, ) { return Err(OperationError { message: "Prepared output destination is stale; select it again." .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 { message: "Overwrite confirmation was not supplied.".to_owned(), }); } destination = destination.confirm_overwrite().map_err(operation_error)?; } if include_qr { app.deliver_first_issue_file_and_qr(identity, attempt, destination) .map(|outcome| match outcome { FirstIssueDeliveryOutcome::Delivered(delivery) => { let (qr, qr_output) = qr_result(delivery.qr); CommandResult::FirstIssue(FirstIssueSummary { identity, status: FirstIssueStatus::Delivered, retry: None, activation_warning: delivery.activation_warning, qr, qr_output, }) } FirstIssueDeliveryOutcome::PublishedButBookkeepingUncertain { value, .. } => { let (qr, qr_output) = qr_result(value.qr); CommandResult::FirstIssue(FirstIssueSummary { identity, status: FirstIssueStatus::PublishedButBookkeepingUncertain, retry: None, activation_warning: value.activation_warning, qr, qr_output, }) } FirstIssueDeliveryOutcome::Failed { error, retry, activation_warning, } => CommandResult::FirstIssue(FirstIssueSummary { identity, status: FirstIssueStatus::Failed(error), retry: Some(retry), activation_warning, qr: QrStatus::NotRequested, qr_output: None, }), }) } else { app.deliver_first_issue_file(identity, attempt, destination) .map(|outcome| match outcome { FirstIssueDeliveryOutcome::Delivered(delivery) => { CommandResult::FirstIssue(FirstIssueSummary { identity, status: FirstIssueStatus::Delivered, retry: None, activation_warning: delivery.activation_warning, qr: QrStatus::NotRequested, qr_output: None, }) } FirstIssueDeliveryOutcome::PublishedButBookkeepingUncertain { value, .. } => CommandResult::FirstIssue(FirstIssueSummary { identity, status: FirstIssueStatus::PublishedButBookkeepingUncertain, retry: None, activation_warning: value.activation_warning, qr: QrStatus::NotRequested, qr_output: None, }), FirstIssueDeliveryOutcome::Failed { error, retry, activation_warning, } => CommandResult::FirstIssue(FirstIssueSummary { identity, status: FirstIssueStatus::Failed(error), retry: Some(retry), activation_warning, qr: QrStatus::NotRequested, qr_output: None, }), }) } } UiCommand::PreviewRestore { file_name } => { app.preview_restore(&file_name).map(|preview| { let changes = preview.change_summary(app.saved_bytes()); let summary = RestorePreviewSummary { file_name: file_name.clone(), created_at_utc: preview.created_at_utc().to_owned(), reason: preview.reason(), historical_apply: preview.historical_apply().clone(), changes, preview_generation: generation, }; self.restore_preview = Some(RestorePreviewContext { _archive_name: file_name, generation, preview, }); CommandResult::RestorePreview(summary) }) } UiCommand::CancelPreparedDelivery => { self.prepared_destination = None; Ok(CommandResult::Cancelled) } UiCommand::CancelRestorePreview => { self.restore_preview = None; Ok(CommandResult::Cancelled) } UiCommand::ConfirmRestore { preview_generation } => { 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( context.generation, preview_generation, generation, ) { return Err(OperationError { message: "Restore preview is stale; select the archive again.".to_owned(), }); } app.confirm_restore(context.preview) .map(CommandResult::Restored) } }; result.map_err(|error| OperationError { message: error.to_string(), }) } fn execute_sensitive( &mut self, app: &mut Application, command: SensitiveCommand, ) -> Result { match command { SensitiveCommand::PrepareInitialSetup(password) => app .prepare_initial_setup_from_sources(password.expose()) .map(CommandResult::InitialSetupPrepared) .map_err(|error| OperationError { message: error.to_string(), }), SensitiveCommand::ReissueClient { identity, password, overwrite_confirmed, prepared_generation, } => { let prepared = self .prepared_destination .take() .ok_or_else(|| OperationError { message: "No prepared output destination is available.".to_owned(), })?; 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 { message: "Overwrite confirmation was not supplied.".to_owned(), }); } destination = destination.confirm_overwrite().map_err(operation_error)?; } let delivery = app .reissue_client_file(identity, password.expose(), destination, include_qr) .map_err(|error| OperationError { message: error.to_string(), })?; let (qr, qr_output) = match delivery.qr { None => (QrStatus::NotRequested, None), Some(result) => qr_result(result), }; Ok(CommandResult::Reissued(ReissueSummary { activation_warning: delivery.activation_warning, qr, qr_output, })) } } } fn execute_sensitive_at( &mut self, app: &mut Application, command: SensitiveCommand, generation: u64, ) -> Result { match &command { SensitiveCommand::ReissueClient { prepared_generation, .. } if *prepared_generation != generation => { return Err(OperationError { message: "Prepared output destination is stale; select it again.".to_owned(), }); } _ => {} } if matches!(command, SensitiveCommand::PrepareInitialSetup(_)) { self.prepared_destination = None; self.restore_preview = None; } self.execute_sensitive(app, command) } } fn operation_error(error: impl std::fmt::Display) -> OperationError { OperationError { message: error.to_string(), } } pub(super) fn destination_generation_matches(stored: u64, requested: u64, current: u64) -> bool { stored == requested && stored == current } pub(super) fn restore_preview_generation_matches( stored: u64, requested: u64, current: u64, ) -> bool { stored == requested && current == requested.saturating_add(1) } fn qr_result(result: Result) -> (QrStatus, Option) { match result { Ok(output) => (QrStatus::Ready, Some(output)), Err(error) => (QrStatus::Failed(error), None), } }