use std::fs::{self, File};
use std::io::Read;
use std::path::Path;
use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectId, ObjectType, RefStatePayload, RefUpdatePayload};
use crate::layout::RepositoryLayout;
use crate::object_store::FileObjectStore;
use crate::refs::log::decode_log_file_bytes;
use crate::refs::pointer::read_ref_pointer;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RefVerification {
pub pointer_count: usize,
pub log_record_count: usize,
}
pub(crate) fn verify_refs(layout: &RepositoryLayout) -> Result<RefVerification> {
let object_store = FileObjectStore::new(layout.clone());
let pointer_count = verify_ref_pointers(layout, &object_store)?;
let log_count = verify_ref_logs(layout, &object_store)?;
Ok(RefVerification {
pointer_count,
log_record_count: log_count,
})
}
fn verify_ref_pointers(layout: &RepositoryLayout, object_store: &FileObjectStore) -> Result<usize> {
let dir = layout.refs_dir().join("by-id");
if !dir.exists() {
return Ok(0);
}
let mut checked = 0_usize;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
return Err(PrikkError::Integrity(format!(
"unexpected directory in ref pointer directory: {}",
path.display()
)));
}
if is_temporary_path(&path) {
continue;
}
ensure_ref_path_shape(&path, ".ref")?;
let pointer = read_ref_pointer(&path)?;
let expected_path = layout.ref_pointer_path(&pointer.ref_name);
if path != expected_path {
return Err(PrikkError::Integrity(format!(
"ref pointer {} does not match canonical path {}",
path.display(),
expected_path.display()
)));
}
let payload = verified_ref_state_payload(object_store, pointer.ref_state_id)?;
if payload.ref_name != pointer.ref_name {
return Err(PrikkError::Integrity(format!(
"RefState {} name mismatch: pointer {}, payload {}",
pointer.ref_state_id, pointer.ref_name, payload.ref_name
)));
}
ensure_block_exists(object_store, payload.target_object_id, pointer.ref_state_id)?;
checked = checked
.checked_add(1)
.ok_or_else(|| PrikkError::Integrity("ref pointer count overflow".to_string()))?;
}
Ok(checked)
}
fn verify_ref_logs(layout: &RepositoryLayout, object_store: &FileObjectStore) -> Result<usize> {
let dir = layout.refs_dir().join("logs");
if !dir.exists() {
return Ok(0);
}
let mut checked = 0_usize;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
return Err(PrikkError::Integrity(format!(
"unexpected directory in ref-log directory: {}",
path.display()
)));
}
if is_temporary_path(&path) {
continue;
}
ensure_ref_path_shape(&path, ".log")?;
let mut bytes = Vec::new();
File::open(path)?.read_to_end(&mut bytes)?;
let replay = decode_log_file_bytes(&bytes)?;
if replay.trailing_partial_bytes != 0 {
return Err(PrikkError::Integrity(
"ref log contains trailing partial record".to_string(),
));
}
let mut previous_ref_state_id = None;
for record in &replay.records {
let payload = RefUpdatePayload::decode_canonical(&record.envelope.canonical_payload)?;
if payload.old_ref_state_id != previous_ref_state_id {
return Err(PrikkError::Integrity(format!(
"ref-log chain mismatch for {} at update {}",
payload.ref_name, payload.update_seq
)));
}
let ref_state_payload =
verified_ref_state_payload(object_store, payload.new_ref_state_id)?;
if ref_state_payload.ref_name != payload.ref_name {
return Err(PrikkError::Integrity(format!(
"RefUpdate points to RefState with different ref name: {} vs {}",
payload.ref_name, ref_state_payload.ref_name
)));
}
if ref_state_payload.previous_ref_state_id != payload.old_ref_state_id {
return Err(PrikkError::Integrity(format!(
"RefState previous link disagrees with RefUpdate for {}",
payload.ref_name
)));
}
if ref_state_payload.target_object_id != payload.new_target_object_id {
return Err(PrikkError::Integrity(format!(
"RefState target disagrees with RefUpdate for {}",
payload.ref_name
)));
}
ensure_block_exists(
object_store,
payload.new_target_object_id,
payload.new_ref_state_id,
)?;
previous_ref_state_id = Some(payload.new_ref_state_id);
}
checked = checked
.checked_add(replay.records.len())
.ok_or_else(|| PrikkError::Integrity("ref-log count overflow".to_string()))?;
}
Ok(checked)
}
fn verified_ref_state_payload(
object_store: &FileObjectStore,
ref_state_id: ObjectId,
) -> Result<RefStatePayload> {
let Some(envelope) = object_store.read_typed(ref_state_id, ObjectType::RefState)? else {
return Err(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)
}
fn ensure_block_exists(
object_store: &FileObjectStore,
block_id: ObjectId,
owner: ObjectId,
) -> Result<()> {
let exists = object_store
.read_typed(block_id, ObjectType::Block)?
.is_some();
if exists {
return Ok(());
}
Err(PrikkError::Integrity(format!(
"ref object {owner} targets missing block {block_id}"
)))
}
fn ensure_ref_path_shape(path: &Path, extension: &str) -> Result<()> {
let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
return Err(PrikkError::Integrity(format!(
"ref path is not valid UTF-8: {}",
path.display()
)));
};
let expected_len = 64_usize
.checked_add(extension.len())
.ok_or_else(|| PrikkError::Integrity("ref extension length overflow".to_string()))?;
if file_name.len() != expected_len || !file_name.ends_with(extension) {
return Err(PrikkError::Integrity(format!(
"ref path does not use sha256hex{} shape: {}",
extension,
path.display()
)));
}
Ok(())
}
fn is_temporary_path(path: &Path) -> bool {
path.file_name()
.and_then(|value| value.to_str())
.map(|value| value.contains(".tmp"))
.unwrap_or(false)
}