use std::path::PathBuf;
mod objects;
mod ref_publication;
mod trust;
use prikk_error::{PrikkError, Result};
use prikk_object::{BlockPayload, ObjectId, ObjectType};
use crate::active::{ActiveRefMetadata, read_active_ref_metadata};
use crate::commit_index::{CommitIndexDivergence, verify_divergence};
use crate::layout::{RepositoryFormat, RepositoryLayout};
use crate::lifecycle_cache::incremental::{
LifecycleCacheDivergence, verify_divergence as verify_lifecycle_cache_divergence,
};
use crate::object_store::FileObjectStore;
use crate::refs::verify_refs;
use crate::rollback_verify::{verify_rollback_draft_wal_records, verify_rollback_patch_envelope};
use crate::signature_diagnostics::{
SignatureEnvelopeIssue, SignatureEnvelopeSource, classify_signature_envelope,
};
use crate::trust::PublicationTrustIssue;
use crate::wal::Wal;
use objects::verify_objects;
use trust::PublicationTrustVerifier;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectVerification {
pub object_id: ObjectId,
pub object_type: ObjectType,
pub path: PathBuf,
pub rollback_patch_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepositoryVerification {
pub legacy_state_roots_unverifiable: bool,
pub checked_objects: usize,
pub checked_wal_records: usize,
pub checked_blocks: usize,
pub checked_rollback_blocks: usize,
pub checked_sealed_rollback_patches: usize,
pub persisted_wal_patches: usize,
pub checked_refs: usize,
pub checked_ref_log_records: usize,
pub ref_publication_issues: Vec<crate::refs::RefPublicationIssue>,
pub signature_envelope_issues: Vec<SignatureEnvelopeIssue>,
pub checked_rollback_draft_records: usize,
pub checked_publication_trust_records: usize,
pub publication_trust_issues: Vec<PublicationTrustIssue>,
pub object_temp_paths: Vec<PathBuf>,
pub trailing_partial_wal_bytes: usize,
pub active_wal_metadata_status: ActiveWalMetadataStatus,
pub commit_index_divergences: Vec<CommitIndexDivergence>,
pub lifecycle_cache_divergences: Vec<LifecycleCacheDivergence>,
pub active_wal_ordering_issues: Vec<ActiveWalOrderingIssue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveWalOrderingIssue {
pub index: usize,
pub previous_seq: u64,
pub seq: u64,
}
impl RepositoryVerification {
#[must_use]
pub const fn has_unverifiable_state_roots(&self) -> bool {
self.legacy_state_roots_unverifiable
}
#[must_use]
pub const fn has_trailing_partial_wal(&self) -> bool {
self.trailing_partial_wal_bytes != 0
}
#[must_use]
pub fn has_publication_trust_issues(&self) -> bool {
!self.publication_trust_issues.is_empty()
}
#[must_use]
pub fn has_blocking_ref_publication_issues(&self) -> bool {
self.ref_publication_issues
.iter()
.any(|issue| issue.blocking)
}
#[must_use]
pub const fn has_active_wal_metadata_integrity_issue(&self) -> bool {
self.active_wal_metadata_status.has_integrity_issue()
}
#[must_use]
pub const fn has_active_wal_metadata_warning(&self) -> bool {
self.active_wal_metadata_status.has_local_debris_warning()
}
#[must_use]
pub fn has_commit_index_divergence(&self) -> bool {
!self.commit_index_divergences.is_empty()
}
#[must_use]
pub fn has_lifecycle_cache_divergence(&self) -> bool {
!self.lifecycle_cache_divergences.is_empty()
}
#[must_use]
pub fn has_active_wal_ordering_issue(&self) -> bool {
!self.active_wal_ordering_issues.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActiveWalMetadataStatus {
MissingForEmptyWal,
ValidForEmptyWal {
ref_name: String,
},
InvalidForEmptyWal {
reason: String,
},
ValidForNonEmptyWal {
ref_name: String,
},
MissingForNonEmptyWal,
InvalidForNonEmptyWal {
reason: String,
},
}
impl ActiveWalMetadataStatus {
#[must_use]
pub const fn has_integrity_issue(&self) -> bool {
matches!(
self,
Self::MissingForNonEmptyWal | Self::InvalidForNonEmptyWal { .. }
)
}
#[must_use]
pub const fn has_local_debris_warning(&self) -> bool {
matches!(
self,
Self::ValidForEmptyWal { .. } | Self::InvalidForEmptyWal { .. }
)
}
}
pub fn verify_repository(layout: &RepositoryLayout) -> Result<RepositoryVerification> {
let object_store = FileObjectStore::new(layout.clone());
let mut trust_verifier = PublicationTrustVerifier::new(layout);
let object_summary = verify_objects(layout, &object_store, &mut trust_verifier)?;
let ref_verification = verify_refs(layout)?;
for envelope in &ref_verification.ref_update_envelopes {
crate::format::validate_read_schema(layout.format(), envelope)?;
trust_verifier.verify(envelope)?;
}
let wal = Wal::for_layout(layout);
let replay = wal.replay()?;
let persisted_wal_patches = verify_wal_persistence(&object_store, &replay.records)?;
let checked_rollback_draft_records = verify_rollback_draft_wal_records(&replay.records)?;
let mut signature_envelope_issues = object_summary.signature_issues;
for record in &replay.records {
crate::format::validate_read_schema(layout.format(), &record.envelope)?;
signature_envelope_issues.extend(classify_signature_envelope(
&record.envelope,
SignatureEnvelopeSource::ActiveWal {
sequence: record.seq,
object_id: record.envelope.object_id(),
},
)?);
}
signature_envelope_issues.extend(ref_verification.signature_envelope_issues);
let active_wal_metadata_status =
classify_active_wal_metadata(layout, replay.records.is_empty())?;
let mut ref_publication_issues = ref_verification.publication_issues;
ref_publication::require_retained_evidence(
layout,
&replay.records,
&active_wal_metadata_status,
trust_verifier.issues.is_empty(),
&mut ref_publication_issues,
)?;
let commit_index_divergences = verify_divergence(layout)?;
let lifecycle_cache_divergences = verify_lifecycle_cache_divergence(&object_store, layout);
let active_wal_ordering_issues = check_active_wal_ordering(&replay.records);
Ok(RepositoryVerification {
legacy_state_roots_unverifiable: layout.format() == RepositoryFormat::LegacyV1,
checked_objects: object_summary.object_count,
checked_wal_records: replay.records.len(),
checked_blocks: object_summary.block_count,
checked_rollback_blocks: object_summary.rollback_block_count,
checked_sealed_rollback_patches: object_summary.rollback_patch_count,
persisted_wal_patches,
checked_refs: ref_verification.pointer_count,
checked_ref_log_records: ref_verification.log_record_count,
ref_publication_issues,
signature_envelope_issues,
checked_rollback_draft_records,
checked_publication_trust_records: trust_verifier.checked_records,
publication_trust_issues: trust_verifier.issues,
object_temp_paths: object_summary.temp_paths,
trailing_partial_wal_bytes: replay.trailing_partial_bytes,
active_wal_metadata_status,
commit_index_divergences,
lifecycle_cache_divergences,
active_wal_ordering_issues,
})
}
fn check_active_wal_ordering(records: &[crate::wal::WalRecord]) -> Vec<ActiveWalOrderingIssue> {
records
.iter()
.zip(records.iter().skip(1))
.enumerate()
.filter(|(_, (previous, current))| current.seq <= previous.seq)
.map(|(index, (previous, current))| ActiveWalOrderingIssue {
index: index + 1,
previous_seq: previous.seq,
seq: current.seq,
})
.collect()
}
fn classify_active_wal_metadata(
layout: &RepositoryLayout,
wal_is_empty: bool,
) -> Result<ActiveWalMetadataStatus> {
match (wal_is_empty, read_active_ref_metadata(layout)?) {
(true, ActiveRefMetadata::Missing) => Ok(ActiveWalMetadataStatus::MissingForEmptyWal),
(true, ActiveRefMetadata::Valid(ref_name)) => {
Ok(ActiveWalMetadataStatus::ValidForEmptyWal { ref_name })
}
(true, ActiveRefMetadata::Invalid(reason)) => {
Ok(ActiveWalMetadataStatus::InvalidForEmptyWal { reason })
}
(false, ActiveRefMetadata::Missing) => Ok(ActiveWalMetadataStatus::MissingForNonEmptyWal),
(false, ActiveRefMetadata::Valid(ref_name)) => {
Ok(ActiveWalMetadataStatus::ValidForNonEmptyWal { ref_name })
}
(false, ActiveRefMetadata::Invalid(reason)) => {
Ok(ActiveWalMetadataStatus::InvalidForNonEmptyWal { reason })
}
}
}
fn verify_block_payload(
object_store: &FileObjectStore,
block_id: ObjectId,
format: RepositoryFormat,
canonical_payload: &[u8],
) -> Result<usize> {
let payload = BlockPayload::decode_canonical(canonical_payload)?;
for parent in &payload.parent_block_ids {
ensure_object_exists(
object_store,
ObjectType::Block,
*parent,
"parent block",
block_id,
)?;
}
let mut rollback_patch_count = 0_usize;
for patch in &payload.patch_ids {
let Some(envelope) = object_store.read_typed(*patch, ObjectType::Patch)? else {
return Err(PrikkError::Integrity(format!(
"object {block_id} references missing block patch {patch}"
)));
};
let context = format!("sealed Block {block_id} Patch {patch}");
if verify_rollback_patch_envelope(&envelope, &context)? {
rollback_patch_count = rollback_patch_count.checked_add(1).ok_or_else(|| {
PrikkError::Integrity("sealed rollback patch count overflow".to_string())
})?;
}
}
if let Some(snapshot) = payload.snapshot_blob_ref {
ensure_object_exists(
object_store,
ObjectType::Blob,
snapshot,
"snapshot blob",
block_id,
)?;
}
if format == RepositoryFormat::CurrentV2 {
crate::block_state::verify_block_v2_state(object_store, block_id, &payload)?;
}
Ok(rollback_patch_count)
}
fn ensure_object_exists(
object_store: &FileObjectStore,
object_type: ObjectType,
object_id: ObjectId,
role: &str,
owner: ObjectId,
) -> Result<()> {
let exists = object_store.read_typed(object_id, object_type)?.is_some();
if exists {
return Ok(());
}
Err(PrikkError::Integrity(format!(
"object {owner} references missing {role} {object_id}"
)))
}
fn verify_wal_persistence(
object_store: &FileObjectStore,
records: &[crate::WalRecord],
) -> Result<usize> {
let mut persisted = 0_usize;
for record in records {
if record.envelope.object_type != ObjectType::Patch {
return Err(PrikkError::Integrity(format!(
"active WAL record {} contains {}, expected patch",
record.seq, record.envelope.object_type
)));
}
if object_store.contains_object(ObjectType::Patch, record.envelope.object_id()) {
persisted = persisted.checked_add(1).ok_or_else(|| {
PrikkError::Integrity("persisted WAL patch count overflow".to_string())
})?;
}
}
Ok(persisted)
}
#[cfg(test)]
mod tests;