use prikk_error::{PrikkError, Result};
use prikk_object::{CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType};
use crate::layout::RepositoryLayout;
use crate::patch_inverse::prepare_patch_inverse_plan;
use crate::patch_replay::decode::decode_supported_patch_operations;
use crate::rollback_draft::is_rollback_draft_envelope;
use crate::wal::{Wal, WalRecord};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RollbackDraftVerification {
pub ref_name: String,
pub wal_sequence: u64,
pub draft_patch_id: ObjectId,
pub target_block_id: ObjectId,
pub block_count: usize,
pub patch_count: usize,
pub inverse_operation_count: usize,
pub decoded_operation_count: usize,
}
pub fn verify_active_rollback_draft(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<RollbackDraftVerification> {
let wal = Wal::new(layout.default_queue_wal_path());
let replay = wal.replay()?;
if replay.trailing_partial_bytes != 0 {
return Err(PrikkError::Integrity(format!(
"active WAL has {} trailing partial bytes; run doctor before rollback-draft-verify",
replay.trailing_partial_bytes
)));
}
let Some(record) = single_wal_record(&replay.records)? else {
return Err(PrikkError::Integrity(
"rollback-draft-verify requires exactly one active WAL record".to_string(),
));
};
verify_rollback_marker(record)?;
let inverse = prepare_patch_inverse_plan(layout, ref_name)?;
let expected_payload = inverse.inverse_payload.to_canonical_bytes()?;
if record.envelope.canonical_payload != expected_payload {
return Err(PrikkError::Integrity(
"active rollback draft payload does not match the current inverse plan".to_string(),
));
}
let decoded = decode_supported_patch_operations(&record.envelope.canonical_payload)?;
if decoded.len() != inverse.inverse_operation_count {
return Err(PrikkError::Integrity(format!(
"rollback draft decoded {} operations but inverse plan has {}",
decoded.len(),
inverse.inverse_operation_count
)));
}
Ok(RollbackDraftVerification {
ref_name: ref_name.to_string(),
wal_sequence: record.seq,
draft_patch_id: record.envelope.object_id(),
target_block_id: inverse.target_block_id,
block_count: inverse.block_count,
patch_count: inverse.patch_count,
inverse_operation_count: inverse.inverse_operation_count,
decoded_operation_count: decoded.len(),
})
}
pub(crate) fn verify_rollback_draft_wal_records(records: &[WalRecord]) -> Result<usize> {
let mut rollback_drafts = 0_usize;
for record in records {
let context = format!("rollback draft WAL record {}", record.seq);
if verify_rollback_patch_envelope(&record.envelope, &context)? {
rollback_drafts = rollback_drafts.checked_add(1).ok_or_else(|| {
PrikkError::Integrity("rollback draft WAL count overflow".to_string())
})?;
}
}
Ok(rollback_drafts)
}
pub(crate) fn verify_rollback_patch_envelope(
envelope: &ObjectEnvelope,
context: &str,
) -> Result<bool> {
if !is_rollback_draft_envelope(envelope) {
return Ok(false);
}
if envelope.object_type != ObjectType::Patch {
return Err(PrikkError::Integrity(format!(
"{context} is {}, expected patch",
envelope.object_type
)));
}
let decoded = decode_supported_patch_operations(&envelope.canonical_payload)?;
if decoded.is_empty() {
return Err(PrikkError::Integrity(format!(
"{context} has no supported inverse operations"
)));
}
Ok(true)
}
fn single_wal_record(records: &[WalRecord]) -> Result<Option<&WalRecord>> {
match records {
[] => Ok(None),
[record] => Ok(Some(record)),
_ => Err(PrikkError::LockConflict(
"rollback-draft-verify requires an active WAL containing only the rollback draft"
.to_string(),
)),
}
}
fn verify_rollback_marker(record: &WalRecord) -> Result<()> {
if record.envelope.object_type != ObjectType::Patch {
return Err(PrikkError::Integrity(format!(
"rollback draft WAL record {} contains {}, expected patch",
record.seq, record.envelope.object_type
)));
}
if is_rollback_draft_envelope(&record.envelope) {
return Ok(());
}
Err(PrikkError::InvalidSignature(format!(
"active WAL record {} is not signed as a rollback draft",
record.seq
)))
}
#[cfg(test)]
mod tests;