use std::collections::{BTreeMap, BTreeSet, VecDeque};
use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use prikk_object::{BlockPayload, ObjectId, ObjectType, RefStatePayload};
use crate::layout::RepositoryLayout;
use crate::merge_evidence::ancestors_inclusive;
use crate::object_store::{ObjectReadSnapshot, ObjectReader};
use crate::refs::{RefStore, resolve_ref_tip_block};
pub use prikk_object::PatchSetDigest;
const PATCH_SET_DIGEST_DOMAIN: &[u8] = b"PRIKK-PATCH-SET-DIGEST-v1";
pub fn patch_set_digest_preimage(patch_ids: &[ObjectId]) -> Result<Vec<u8>> {
if !prikk_object::canonical::is_strictly_sorted(patch_ids) {
return Err(PrikkError::Integrity(
"patch-set digest input is not strictly sorted and deduplicated".to_string(),
));
}
let count = u64::try_from(patch_ids.len())
.map_err(|_| PrikkError::Integrity("patch-set digest count exceeds u64".to_string()))?;
let mut preimage = Vec::with_capacity(PATCH_SET_DIGEST_DOMAIN.len() + 8 + patch_ids.len() * 32);
preimage.extend_from_slice(PATCH_SET_DIGEST_DOMAIN);
preimage.extend_from_slice(&count.to_be_bytes());
for patch_id in patch_ids {
preimage.extend_from_slice(patch_id.as_bytes());
}
Ok(preimage)
}
pub fn compute_patch_set_digest(patch_ids: &[ObjectId]) -> Result<PatchSetDigest> {
Ok(PatchSetDigest(sha256(&patch_set_digest_preimage(
patch_ids,
)?)))
}
pub fn patch_ids_reachable_from_block(
object_store: &impl ObjectReader,
tip_block_id: ObjectId,
) -> Result<Vec<ObjectId>> {
let ancestors = ancestors_inclusive(object_store, tip_block_id)?;
let mut patch_ids: BTreeSet<ObjectId> = BTreeSet::new();
for block in ancestors.values() {
patch_ids.extend(block.patch_ids.iter().copied());
}
Ok(patch_ids.into_iter().collect())
}
pub fn compute_patch_set_digest_from_block(
object_store: &impl ObjectReader,
tip_block_id: ObjectId,
) -> Result<PatchSetDigest> {
compute_patch_set_digest(&patch_ids_reachable_from_block(object_store, tip_block_id)?)
}
pub fn compute_patch_set_digest_and_count_from_block(
object_store: &impl ObjectReader,
tip_block_id: ObjectId,
) -> Result<(PatchSetDigest, u64)> {
let patch_ids = patch_ids_reachable_from_block(object_store, tip_block_id)?;
let count = crate::fsutil::len_to_u64(patch_ids.len())?;
Ok((compute_patch_set_digest(&patch_ids)?, count))
}
fn resolve_ref_to_tip_block(
layout: &RepositoryLayout,
object_store: &impl ObjectReader,
ref_name: &str,
) -> Result<ObjectId> {
if ref_name.starts_with("remotes/") {
return Err(PrikkError::Integrity(format!(
"patch-set digest does not support received refs ({ref_name}) yet -- the received \
namespace is what patch-level exchange itself restructures (RFC 115 design D2/D5); \
its digest semantics belong to a later stage, not assumed here"
)));
}
let ref_store = RefStore::new(layout.clone());
let Some(ref_state_id) = ref_store.read_current_ref_state_id(ref_name)? else {
return Err(PrikkError::Integrity(format!(
"ref {ref_name} does not exist, nothing to compute a patch-set digest for"
)));
};
let ref_state_envelope = object_store
.read_typed(ref_state_id, ObjectType::RefState)?
.ok_or_else(|| PrikkError::Integrity(format!("missing RefState object: {ref_state_id}")))?;
let ref_state_payload = RefStatePayload::decode_canonical(
&ref_state_envelope.canonical_payload,
ref_state_envelope.schema_version,
)?;
let (target_block_id, _tag_envelope) = resolve_ref_tip_block(object_store, &ref_state_payload)?;
Ok(target_block_id)
}
pub fn compute_patch_set_digest_for_ref(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<PatchSetDigest> {
let object_store = ObjectReadSnapshot::open(layout)?;
let tip_block_id = resolve_ref_to_tip_block(layout, &object_store, ref_name)?;
compute_patch_set_digest_from_block(&object_store, tip_block_id)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatchSetResolution {
NotHeld,
Resolved(ObjectId),
}
pub fn resolve_patch_set_digest(
layout: &RepositoryLayout,
digest: PatchSetDigest,
patch_count: u64,
) -> Result<PatchSetResolution> {
let object_store = ObjectReadSnapshot::open(layout)?;
let ref_store = RefStore::new(layout.clone());
let mut candidates: BTreeMap<ObjectId, BlockPayload> = BTreeMap::new();
for pointer in ref_store.list_ref_pointers()? {
let tip_block_id = resolve_ref_to_tip_block(layout, &object_store, &pointer.ref_name)?;
candidates.extend(ancestors_inclusive(&object_store, tip_block_id)?);
}
let mut remaining_parents: BTreeMap<ObjectId, usize> = BTreeMap::new();
let mut children: BTreeMap<ObjectId, Vec<ObjectId>> = BTreeMap::new();
for (&block_id, block) in &candidates {
remaining_parents.insert(block_id, block.parent_block_ids.len());
for &parent_id in &block.parent_block_ids {
children.entry(parent_id).or_default().push(block_id);
}
}
let mut remaining_children: BTreeMap<ObjectId, usize> = candidates
.keys()
.map(|&block_id| {
let count = children.get(&block_id).map_or(0, Vec::len);
(block_id, count)
})
.collect();
let mut ready: Vec<ObjectId> = remaining_parents
.iter()
.filter(|&(_, &count)| count == 0)
.map(|(&block_id, _)| block_id)
.collect();
ready.sort_unstable();
let mut queue: VecDeque<ObjectId> = ready.into();
let mut live_closures: BTreeMap<ObjectId, BTreeSet<ObjectId>> = BTreeMap::new();
let mut matches: Vec<ObjectId> = Vec::new();
while let Some(block_id) = queue.pop_front() {
let block = candidates.get(&block_id).ok_or_else(|| {
PrikkError::Integrity(
"patch-set digest resolution lost a tracked candidate -- internal inconsistency"
.to_string(),
)
})?;
let mut closure: BTreeSet<ObjectId> = BTreeSet::new();
for &parent_id in &block.parent_block_ids {
let parent_closure =
take_parent_closure(parent_id, &mut live_closures, &mut remaining_children)?;
if closure.is_empty() {
closure = parent_closure;
} else {
closure.extend(parent_closure);
}
}
closure.extend(block.patch_ids.iter().copied());
if crate::fsutil::len_to_u64(closure.len())? == patch_count {
let sorted: Vec<ObjectId> = closure.iter().copied().collect();
if compute_patch_set_digest(&sorted)? == digest {
matches.push(block_id);
}
}
if remaining_children.get(&block_id).copied().unwrap_or(0) > 0 {
live_closures.insert(block_id, closure);
}
for &child_id in children.get(&block_id).into_iter().flatten() {
let entry = remaining_parents.get_mut(&child_id).ok_or_else(|| {
PrikkError::Integrity(
"patch-set digest resolution lost a tracked child -- internal inconsistency"
.to_string(),
)
})?;
*entry -= 1;
if *entry == 0 {
queue.push_back(child_id);
}
}
}
match matches.len() {
0 => Ok(PatchSetResolution::NotHeld),
1 => {
let block_id = matches.pop().ok_or_else(|| {
PrikkError::Integrity(
"patch-set digest resolution: exactly one match reported but none present -- \
internal inconsistency"
.to_string(),
)
})?;
Ok(PatchSetResolution::Resolved(block_id))
}
_ => {
matches.sort_unstable();
let names = matches
.iter()
.map(ObjectId::to_string)
.collect::<Vec<_>>()
.join(", ");
Err(PrikkError::Integrity(format!(
"patch-set digest resolves to {} distinct local blocks, refusing to pick: {names}",
matches.len()
)))
}
}
}
fn take_parent_closure(
parent_id: ObjectId,
live_closures: &mut BTreeMap<ObjectId, BTreeSet<ObjectId>>,
remaining_children: &mut BTreeMap<ObjectId, usize>,
) -> Result<BTreeSet<ObjectId>> {
let count = remaining_children.get_mut(&parent_id).ok_or_else(|| {
PrikkError::Integrity(
"patch-set digest resolution lost a parent's remaining-child count -- internal \
inconsistency"
.to_string(),
)
})?;
*count = count.checked_sub(1).ok_or_else(|| {
PrikkError::Integrity(
"patch-set digest resolution consumed a parent's closure more times than it has \
children -- internal inconsistency"
.to_string(),
)
})?;
if *count == 0 {
live_closures.remove(&parent_id).ok_or_else(|| {
PrikkError::Integrity(format!(
"patch-set digest resolution: parent {parent_id} has no live closure to take"
))
})
} else {
live_closures.get(&parent_id).cloned().ok_or_else(|| {
PrikkError::Integrity(format!(
"patch-set digest resolution: parent {parent_id} has no live closure to clone"
))
})
}
}
#[cfg(test)]
mod tests;