use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectId, ObjectType, RefKind, RefStatePayload};
use crate::byte_cursor::ByteCursor;
use crate::file_codec::{push_string_u16, push_u64};
use crate::fsutil::len_to_u64;
use crate::layout::RepositoryLayout;
use crate::object_store::{ObjectReadSnapshot, ObjectReader};
use crate::patch_set_digest::{
PatchSetDigest, compute_patch_set_digest, patch_ids_reachable_from_block,
};
use crate::refs::{RefStore, validate_local_branch_ref};
const HAVE_LIST_MAGIC: &[u8; 8] = b"PSYNCHV1";
pub const DEFAULT_HAVE_LIST_MAX_PATCH_COUNT: usize = 100_000;
pub const DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES: usize = 64 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HaveList {
pub ref_name: String,
pub digest: PatchSetDigest,
pub patch_ids: Vec<ObjectId>,
}
pub fn build_have_list(layout: &RepositoryLayout, ref_name: &str) -> Result<Vec<u8>> {
let canonical_ref = validate_local_branch_ref(ref_name)?;
let ref_store = RefStore::new(layout.clone());
let object_store = ObjectReadSnapshot::open(layout)?;
let patch_ids = match ref_store.read_current_ref_state_id(&canonical_ref)? {
Some(ref_state_id) => {
let envelope = object_store
.read_typed(ref_state_id, ObjectType::RefState)?
.ok_or_else(|| {
PrikkError::Integrity(format!(
"ref {canonical_ref} names missing RefState {ref_state_id}"
))
})?;
let payload = RefStatePayload::decode_canonical(
&envelope.canonical_payload,
envelope.schema_version,
)?;
if payload.kind != RefKind::Branch {
return Err(PrikkError::Integrity(format!(
"ref {canonical_ref} is under heads/ but its RefState kind is not Branch"
)));
}
patch_ids_reachable_from_block(&object_store, payload.target_object_id)?
}
None => Vec::new(),
};
let digest = compute_patch_set_digest(&patch_ids)?;
let mut out = Vec::new();
out.extend_from_slice(HAVE_LIST_MAGIC);
push_string_u16(&mut out, &canonical_ref)?;
out.extend_from_slice(&digest.0);
push_u64(&mut out, len_to_u64(patch_ids.len())?);
for patch_id in &patch_ids {
out.extend_from_slice(patch_id.as_bytes());
}
Ok(out)
}
pub fn decode_have_list(
bytes: &[u8],
max_total_bytes: usize,
max_patch_count: usize,
) -> Result<HaveList> {
if bytes.len() > max_total_bytes {
return Err(PrikkError::MalformedData(format!(
"have-list is {} bytes, over the configured limit of {max_total_bytes} bytes",
bytes.len()
)));
}
let mut cursor = ByteCursor::new(bytes);
let magic = cursor.read_array::<8>()?;
if &magic != HAVE_LIST_MAGIC {
return Err(PrikkError::MalformedData(
"invalid have-list magic".to_string(),
));
}
let ref_name = cursor.read_string_u16()?;
let declared_digest = PatchSetDigest(cursor.read_array::<32>()?);
let patch_count = cursor.read_u64()?;
if patch_count > len_to_u64(max_patch_count)? {
return Err(PrikkError::MalformedData(format!(
"have-list declares {patch_count} patch ids, over the configured limit of \
{max_patch_count}"
)));
}
let mut patch_ids = Vec::new();
for _ in 0..patch_count {
patch_ids.push(ObjectId::from_bytes(cursor.read_array::<32>()?));
}
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in have-list".to_string(),
));
}
let recomputed_digest = compute_patch_set_digest(&patch_ids)?;
if recomputed_digest != declared_digest {
return Err(PrikkError::Integrity(
"have-list's declared patch-set digest does not match its own carried list".to_string(),
));
}
Ok(HaveList {
ref_name,
digest: declared_digest,
patch_ids,
})
}
#[cfg(test)]
mod tests;