use std::collections::HashSet;
use std::path::{Path, PathBuf};
use prikk_error::{PrikkError, Result};
use prikk_object::{BlockPayload, ObjectEnvelope, ObjectId, ObjectType};
use super::{
AuthorSignatureVerification, BlockSealVerification, ObjectVerification,
PublicationTrustVerifier, verify_block_payload,
};
use crate::block_state::{BlockStateOutcome, LineageStateMemo, verify_blocks_topological};
use crate::container::{self, ContainerRecordStatus};
use crate::fsutil::{EntryKind, inspect_entry, list_directory, read_file_if_exists};
use crate::index::replay_index;
use crate::layout::{ContainerSlot, RepositoryLayout, persisted_object_types};
use crate::object_store::ObjectReader;
use crate::signature_diagnostics::{
SignatureEnvelopeIssue, SignatureEnvelopeSource, classify_signature_envelope,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ObjectItemStatus {
Evaluated(ObjectVerification),
Unindexed(ObjectVerification),
Failed {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectItemOutcome {
pub object_type: ObjectType,
pub path: PathBuf,
pub status: ObjectItemStatus,
}
pub(super) struct ObjectSummary {
pub(super) item_outcomes: Vec<ObjectItemOutcome>,
pub(super) topological_outcomes: Vec<BlockStateOutcome>,
pub(super) temp_paths: Vec<PathBuf>,
pub(super) signature_issues: Vec<SignatureEnvelopeIssue>,
pub(super) merge_baseline_divergences: Vec<super::MergeBaselineDivergence>,
pub(super) block_seals: Vec<BlockSealVerification>,
}
impl ObjectSummary {
fn empty() -> Self {
Self {
item_outcomes: Vec::new(),
topological_outcomes: Vec::new(),
temp_paths: Vec::new(),
signature_issues: Vec::new(),
merge_baseline_divergences: Vec::new(),
block_seals: Vec::new(),
}
}
fn add(&mut self, other: Self) {
self.item_outcomes.extend(other.item_outcomes);
self.topological_outcomes.extend(other.topological_outcomes);
self.temp_paths.extend(other.temp_paths);
self.signature_issues.extend(other.signature_issues);
self.merge_baseline_divergences
.extend(other.merge_baseline_divergences);
self.block_seals.extend(other.block_seals);
}
}
pub(super) fn verify_objects(
layout: &RepositoryLayout,
object_store: &impl ObjectReader,
trust_verifier: &mut PublicationTrustVerifier<'_>,
) -> Result<ObjectSummary> {
let mut lineage_memo = LineageStateMemo::new();
let mut pending_v3_blocks: Vec<(ObjectId, BlockPayload)> = Vec::new();
let mut summary = ObjectSummary::empty();
let index_replay = replay_index(layout)?;
if index_replay.has_item_failure() {
return Err(PrikkError::Integrity(
"object index has a damaged entry; run doctor before verify can classify indexing"
.to_string(),
));
}
let indexed_ids: HashSet<ObjectId> = index_replay
.entries
.iter()
.map(|entry| entry.object_id)
.collect();
for entry in &index_replay.entries {
let Ok(envelope) = crate::index::read_object_envelope_at(layout, entry) else {
continue;
};
let computed = envelope.object_id();
if computed != entry.object_id {
return Err(PrikkError::Integrity(format!(
"index entry for {} resolves to an envelope with computed id {computed}",
entry.object_id
)));
}
}
for object_type in persisted_object_types() {
summary.add(verify_object_type_container(
layout,
object_store,
object_type,
trust_verifier,
&mut pending_v3_blocks,
&indexed_ids,
)?);
}
summary.temp_paths = scan_loose_file_temp_debris(layout)?;
let topological =
verify_blocks_topological(object_store, &pending_v3_blocks, &mut lineage_memo)?;
summary.topological_outcomes = topological.outcomes;
Ok(summary)
}
fn verify_object_type_container(
layout: &RepositoryLayout,
object_store: &impl ObjectReader,
object_type: ObjectType,
trust_verifier: &mut PublicationTrustVerifier<'_>,
pending_v3_blocks: &mut Vec<(ObjectId, BlockPayload)>,
indexed_ids: &HashSet<ObjectId>,
) -> Result<ObjectSummary> {
let mut summary = ObjectSummary::empty();
let container_path = layout.container_slot_path(object_type, ContainerSlot::A);
let relative = layout.repository_relative(&container_path)?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(summary);
};
let replay = container::decode_container_records(object_type, &bytes)?;
let mut records = replay.records.into_iter();
for outcome in &replay.record_outcomes {
let locator = container_path.join(format!("#{}", outcome.offset));
let ContainerRecordStatus::Evaluated { .. } = &outcome.status else {
let ContainerRecordStatus::Failed { message } = &outcome.status else {
unreachable!("record_outcomes only ever holds Evaluated or Failed")
};
summary.item_outcomes.push(ObjectItemOutcome {
object_type,
path: locator,
status: ObjectItemStatus::Failed {
message: message.clone(),
},
});
continue;
};
let Some(record) = records.next() else {
return Err(PrikkError::Integrity(
"container replay outcome/record count mismatch".to_string(),
));
};
match verify_object_record(
layout,
object_store,
object_type,
&locator,
&record.envelope,
trust_verifier,
pending_v3_blocks,
) {
Ok((object, signature_issues, merge_baseline_divergence)) => {
summary.signature_issues.extend(signature_issues);
summary
.merge_baseline_divergences
.extend(merge_baseline_divergence);
if object.object_type == ObjectType::Block {
if let Some(sealed_by_key_id) = object.sealed_by_key_id.clone() {
summary.block_seals.push(BlockSealVerification {
block_id: object.object_id,
sealed_by_key_id,
});
}
}
let status = if indexed_ids.contains(&object.object_id) {
ObjectItemStatus::Evaluated(object)
} else {
ObjectItemStatus::Unindexed(object)
};
summary.item_outcomes.push(ObjectItemOutcome {
object_type,
path: locator,
status,
});
}
Err(err) => {
summary.item_outcomes.push(ObjectItemOutcome {
object_type,
path: locator,
status: ObjectItemStatus::Failed {
message: err.to_string(),
},
});
}
}
}
Ok(summary)
}
fn verify_object_record(
layout: &RepositoryLayout,
object_store: &impl ObjectReader,
object_type: ObjectType,
locator: &Path,
envelope: &ObjectEnvelope,
trust_verifier: &mut PublicationTrustVerifier<'_>,
pending_v3_blocks: &mut Vec<(ObjectId, BlockPayload)>,
) -> Result<(
ObjectVerification,
Vec<SignatureEnvelopeIssue>,
Option<super::MergeBaselineDivergence>,
)> {
crate::format::validate_read_schema(layout.format(), envelope)?;
let object_id = envelope.object_id();
let signature_issues = classify_signature_envelope(
envelope,
SignatureEnvelopeSource::Object {
object_type,
object_id,
},
)?;
let sealed_by_key_id = if matches!(object_type, ObjectType::Block | ObjectType::RefState) {
trust_verifier.verify(envelope)?
} else {
None
};
let author_verification = if object_type == ObjectType::Patch {
crate::author_key_index::verify_author_signature(layout, envelope)?.map(
|(key_id, sound)| {
if sound {
AuthorSignatureVerification::Sound { key_id }
} else {
AuthorSignatureVerification::Unverifiable { key_id }
}
},
)
} else {
None
};
let (rollback_patch_count, merge_baseline_divergence) = if object_type == ObjectType::Block {
verify_block_payload(
object_store,
object_id,
layout.format(),
&envelope.canonical_payload,
pending_v3_blocks,
)?
} else {
(0, None)
};
Ok((
ObjectVerification {
object_id,
object_type,
path: locator.to_path_buf(),
rollback_patch_count,
sealed_by_key_id,
author_verification,
},
signature_issues,
merge_baseline_divergence,
))
}
fn scan_loose_file_temp_debris(layout: &RepositoryLayout) -> Result<Vec<PathBuf>> {
let mut temp_paths = Vec::new();
for object_type in persisted_object_types() {
let dir = layout.object_type_dir(object_type);
let relative_dir = layout.repository_relative(&dir)?;
match inspect_entry(layout.repository_mutation_root(), &relative_dir)? {
None => continue,
Some(EntryKind::Directory) => {}
Some(_) => {
return Err(PrikkError::Integrity(format!(
"unexpected non-directory in object type directory: {}",
dir.display()
)));
}
}
let mut prefix_entries = list_directory(layout.repository_mutation_root(), &relative_dir)?;
prefix_entries.sort_by(|left, right| {
left.name
.as_encoded_bytes()
.cmp(right.name.as_encoded_bytes())
});
for prefix_entry in prefix_entries {
let prefix_path = dir.join(&prefix_entry.name);
if prefix_entry.kind != EntryKind::Directory {
return Err(PrikkError::Integrity(format!(
"unexpected non-directory in object type directory: {}",
prefix_path.display()
)));
}
let relative_prefix = layout.repository_relative(&prefix_path)?;
let mut entries = list_directory(layout.repository_mutation_root(), &relative_prefix)?;
entries.sort_by(|left, right| {
left.name
.as_encoded_bytes()
.cmp(right.name.as_encoded_bytes())
});
for entry in entries {
let path = prefix_path.join(&entry.name);
if entry.kind != EntryKind::Regular {
return Err(PrikkError::Integrity(format!(
"unexpected non-file in object prefix directory: {}",
path.display()
)));
}
if is_object_temp_path(&path) {
temp_paths.push(path);
continue;
}
return Err(PrikkError::Integrity(format!(
"unexpected loose object file under format-3 (containers own object storage \
now): {}",
path.display()
)));
}
}
}
Ok(temp_paths)
}
fn is_object_temp_path(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
return false;
};
let Some((object_name, suffix)) = name.split_once(".pobj.tmp.") else {
return false;
};
let Some((pid, random)) = suffix.split_once('.') else {
return false;
};
object_name.len() == 64
&& object_name.bytes().all(|byte| byte.is_ascii_hexdigit())
&& !pid.is_empty()
&& pid.bytes().all(|byte| byte.is_ascii_digit())
&& random.len() == 32
&& random.bytes().all(|byte| byte.is_ascii_hexdigit())
}