use std::collections::BTreeMap;
use std::path::PathBuf;
use prikk_error::{PrikkError, Result};
use prikk_object::{
ObjectEnvelope, ObjectId, RefKind, RefStatePayload, RefUpdatePayload, TagPayload,
};
use crate::layout::{ContainerSlot, RepositoryLayout, ref_name_key_bytes};
use crate::object_store::ObjectReader;
use crate::refs::container::{
RefContainerRecordStatus, RefLogRecordStatus, RefLogReplay, decode_ref_container_records,
replay_ref_subsequence,
};
use crate::refs::pointer_index::{
PointerIndexEntry, PointerIndexRecordStatus, replay_pointer_index,
};
use prikk_object::ObjectType;
#[derive(Debug, Clone)]
pub(super) struct PointerState {
pub(super) id: ObjectId,
pub(super) payload: RefStatePayload,
}
#[derive(Debug, Clone)]
pub(super) struct LogState {
pub(super) tip: Option<ObjectId>,
pub(super) previous_tip: Option<ObjectId>,
pub(super) record_count: usize,
pub(super) trailing_partial_bytes: usize,
pub(super) has_legacy_timestamp: bool,
}
pub(super) struct RefLogEnvelope {
pub(super) ref_name: String,
pub(super) sequence: u64,
pub(super) envelope: ObjectEnvelope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefFileStatus {
Evaluated {
ref_name: String,
},
Failed {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefFileOutcome {
pub path: PathBuf,
pub status: RefFileStatus,
}
fn pointer_locator(layout: &RepositoryLayout, offset: usize) -> PathBuf {
layout
.ref_pointer_index_slot_path(ContainerSlot::A)
.join(format!("#{offset}"))
}
fn log_locator(layout: &RepositoryLayout, offset: usize) -> PathBuf {
layout
.ref_log_container_slot_path(ContainerSlot::A)
.join(format!("#{offset}"))
}
#[allow(clippy::type_complexity)]
pub(super) fn read_pointers(
layout: &RepositoryLayout,
objects: &impl ObjectReader,
) -> Result<(
BTreeMap<String, PointerState>,
BTreeMap<[u8; 32], String>,
Vec<RefFileOutcome>,
)> {
let replay = replay_pointer_index(layout)?;
if replay.has_item_failure() {
return Err(PrikkError::Integrity(
"ref pointer index has a damaged entry; run doctor before verify can classify \
publication state"
.to_string(),
));
}
let mut latest: BTreeMap<[u8; 32], (usize, PointerIndexEntry)> = BTreeMap::new();
for outcome in replay.record_outcomes.iter().zip(replay.entries) {
let (record_outcome, entry) = outcome;
if !matches!(record_outcome.status, PointerIndexRecordStatus::Evaluated) {
continue;
}
latest.insert(entry.ref_name_key, (record_outcome.offset, entry));
}
let mut pointers = BTreeMap::new();
let mut failures_by_key = BTreeMap::new();
let mut outcomes = Vec::new();
for (key, (offset, entry)) in latest {
let locator = pointer_locator(layout, offset);
match read_one_pointer_entry(objects, &entry) {
Ok((ref_name, state)) => {
pointers.insert(ref_name.clone(), state);
outcomes.push(RefFileOutcome {
path: locator,
status: RefFileStatus::Evaluated { ref_name },
});
}
Err(err) => {
failures_by_key.insert(key, err.to_string());
outcomes.push(RefFileOutcome {
path: locator,
status: RefFileStatus::Failed {
message: err.to_string(),
},
});
}
}
}
Ok((pointers, failures_by_key, outcomes))
}
fn read_one_pointer_entry(
objects: &impl ObjectReader,
entry: &PointerIndexEntry,
) -> Result<(String, PointerState)> {
if ref_name_key_bytes(&entry.ref_name) != entry.ref_name_key {
return Err(PrikkError::Integrity(format!(
"pointer index entry ref_name_key does not match sha256({})",
entry.ref_name
)));
}
let payload = verified_ref_state_payload(objects, entry.ref_state_id)?;
if payload.ref_name != entry.ref_name {
return Err(PrikkError::Integrity(format!(
"RefState {} name differs from pointer ref {}",
entry.ref_state_id, entry.ref_name
)));
}
ensure_ref_target_valid(
objects,
payload.kind,
payload.target_object_id,
entry.ref_state_id,
)?;
Ok((
entry.ref_name.clone(),
PointerState {
id: entry.ref_state_id,
payload,
},
))
}
#[allow(clippy::type_complexity)]
pub(super) fn read_logs(
layout: &RepositoryLayout,
objects: &impl ObjectReader,
_pointers: &BTreeMap<String, PointerState>,
) -> Result<(
BTreeMap<String, LogState>,
usize,
Vec<RefLogEnvelope>,
BTreeMap<[u8; 32], String>,
Vec<RefFileOutcome>,
)> {
let relative =
layout.repository_relative(&layout.ref_log_container_slot_path(ContainerSlot::A))?;
let Some(bytes) =
crate::fsutil::read_file_if_exists(layout.repository_mutation_root(), &relative)?
else {
return Ok((BTreeMap::new(), 0, Vec::new(), BTreeMap::new(), Vec::new()));
};
let discovery = decode_ref_container_records(&bytes)?;
let mut keys: std::collections::BTreeSet<[u8; 32]> = discovery
.records
.iter()
.map(|record| record.ref_name_key)
.collect();
keys.extend(
discovery
.record_outcomes
.iter()
.filter_map(|outcome| match &outcome.status {
RefContainerRecordStatus::Failed {
claimed_ref_name_key: Some(key),
..
} => Some(*key),
_ => None,
}),
);
let mut logs = BTreeMap::new();
let mut total = 0_usize;
let mut envelopes = Vec::new();
let mut failures_by_key = BTreeMap::new();
let mut outcomes = Vec::new();
for key in keys {
let replay = replay_ref_subsequence(layout, key)?;
let first_offset = replay
.record_outcomes
.first()
.map_or(0, |outcome| outcome.offset);
if replay.has_item_failure() {
let joined = replay
.record_outcomes
.iter()
.filter_map(|outcome| match &outcome.status {
RefLogRecordStatus::Failed { message } => {
Some(format!("offset {}: {message}", outcome.offset))
}
RefLogRecordStatus::Evaluated => None,
})
.collect::<Vec<_>>()
.join("; ");
let message = format!("ref log has damaged record(s): {joined}");
failures_by_key.insert(key, message.clone());
outcomes.push(RefFileOutcome {
path: log_locator(layout, first_offset),
status: RefFileStatus::Failed { message },
});
continue;
}
match validate_log_replay(objects, key, &replay) {
Ok(Some((ref_name, state, record_envelopes))) => {
total = match total.checked_add(record_envelopes.len()) {
Some(value) => value,
None => {
return Err(PrikkError::Integrity("ref-log count overflow".to_string()));
}
};
envelopes.extend(record_envelopes);
logs.insert(ref_name.clone(), state);
outcomes.push(RefFileOutcome {
path: log_locator(layout, first_offset),
status: RefFileStatus::Evaluated { ref_name },
});
}
Ok(None) => {}
Err(err) => {
failures_by_key.insert(key, err.to_string());
outcomes.push(RefFileOutcome {
path: log_locator(layout, first_offset),
status: RefFileStatus::Failed {
message: err.to_string(),
},
});
}
}
}
Ok((logs, total, envelopes, failures_by_key, outcomes))
}
#[allow(clippy::type_complexity)]
fn validate_log_replay(
objects: &impl ObjectReader,
ref_name_key: [u8; 32],
replay: &RefLogReplay,
) -> Result<Option<(String, LogState, Vec<RefLogEnvelope>)>> {
if replay.records.is_empty() {
if replay.trailing_partial_bytes == 0 {
return Ok(None);
}
return Err(PrikkError::Integrity(
"ref log has a trailing partial record with no sound records of its own".to_string(),
));
}
let mut previous = None;
let mut previous_tip = None;
let mut ref_name = None;
let mut has_legacy_timestamp = false;
let mut record_envelopes = Vec::with_capacity(replay.records.len());
for (index, record) in replay.records.iter().enumerate() {
let update = RefUpdatePayload::decode_canonical(&record.envelope.canonical_payload)?;
if crate::layout::ref_name_key_bytes(&update.ref_name) != ref_name_key {
return Err(PrikkError::Integrity(
"ref container record header ref_name_key does not match its own envelope"
.to_string(),
));
}
let expected_seq = u64::try_from(index)
.ok()
.and_then(|value| value.checked_add(1))
.ok_or_else(|| PrikkError::Integrity("ref-log sequence overflow".to_string()))?;
if ref_name
.as_ref()
.is_some_and(|name| name != &update.ref_name)
|| update.old_ref_state_id != previous
|| update.update_seq != expected_seq
{
return Err(PrikkError::Integrity(format!(
"ref-log chain or sequence diverges for {}",
update.ref_name
)));
}
verify_update(objects, &update)?;
has_legacy_timestamp |= update.created_at != 0;
ref_name.get_or_insert_with(|| update.ref_name.clone());
previous_tip = previous;
previous = Some(update.new_ref_state_id);
record_envelopes.push(RefLogEnvelope {
ref_name: update.ref_name.clone(),
sequence: update.update_seq,
envelope: record.envelope.clone(),
});
}
let name = ref_name
.ok_or_else(|| PrikkError::Integrity("non-empty ref log has no identity".to_string()))?;
Ok(Some((
name,
LogState {
tip: previous,
previous_tip,
record_count: replay.records.len(),
trailing_partial_bytes: replay.trailing_partial_bytes,
has_legacy_timestamp,
},
record_envelopes,
)))
}
fn verify_update(objects: &impl ObjectReader, update: &RefUpdatePayload) -> Result<()> {
let state = verified_ref_state_payload(objects, update.new_ref_state_id)?;
if state.ref_name != update.ref_name
|| state.previous_ref_state_id != update.old_ref_state_id
|| state.target_object_id != update.new_target_object_id
|| state.update_seq != update.update_seq
{
return Err(PrikkError::Integrity(format!(
"RefState disagrees with RefUpdate for {}",
update.ref_name
)));
}
ensure_ref_target_valid(
objects,
state.kind,
update.new_target_object_id,
update.new_ref_state_id,
)
}
fn verified_ref_state_payload(
objects: &impl ObjectReader,
ref_state_id: ObjectId,
) -> Result<RefStatePayload> {
let envelope = objects
.read_typed(ref_state_id, ObjectType::RefState)?
.ok_or_else(|| PrikkError::Integrity(format!("missing RefState object: {ref_state_id}")))?;
if envelope.signatures.is_empty() {
return Err(PrikkError::Integrity(format!(
"RefState {ref_state_id} is unsigned"
)));
}
RefStatePayload::decode_canonical(&envelope.canonical_payload, envelope.schema_version)
}
pub(crate) fn ensure_ref_target_valid(
objects: &impl ObjectReader,
kind: RefKind,
target_object_id: ObjectId,
owner: ObjectId,
) -> Result<()> {
match kind {
RefKind::Branch => ensure_block_exists(objects, target_object_id, owner),
RefKind::Tag => {
let tag_envelope = objects
.read_typed(target_object_id, ObjectType::Tag)?
.ok_or_else(|| {
PrikkError::Integrity(format!(
"ref object {owner} targets missing tag {target_object_id}"
))
})?;
let tag_payload = TagPayload::decode_canonical(&tag_envelope.canonical_payload)?;
ensure_block_exists(objects, tag_payload.target_block_id, owner)
}
}
}
fn ensure_block_exists(
objects: &impl ObjectReader,
block_id: ObjectId,
owner: ObjectId,
) -> Result<()> {
if objects.read_typed(block_id, ObjectType::Block)?.is_some() {
return Ok(());
}
Err(PrikkError::Integrity(format!(
"ref object {owner} targets missing block {block_id}"
)))
}