prikk_store/sync_negotiation/
have_list.rs1use prikk_error::{PrikkError, Result};
15use prikk_object::{ObjectId, ObjectType, RefKind, RefStatePayload};
16
17use crate::byte_cursor::ByteCursor;
18use crate::file_codec::{push_string_u16, push_u64};
19use crate::fsutil::len_to_u64;
20use crate::layout::RepositoryLayout;
21use crate::object_store::{ObjectReadSnapshot, ObjectReader};
22use crate::patch_set_digest::{
23 PatchSetDigest, compute_patch_set_digest, patch_ids_reachable_from_block,
24};
25use crate::refs::{RefStore, validate_local_branch_ref};
26
27const HAVE_LIST_MAGIC: &[u8; 8] = b"PSYNCHV1";
28
29pub const DEFAULT_HAVE_LIST_MAX_PATCH_COUNT: usize = 100_000;
31
32pub const DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES: usize = 64 * 1024 * 1024;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct HaveList {
38 pub ref_name: String,
40 pub digest: PatchSetDigest,
42 pub patch_ids: Vec<ObjectId>,
45}
46
47pub fn build_have_list(layout: &RepositoryLayout, ref_name: &str) -> Result<Vec<u8>> {
53 let canonical_ref = validate_local_branch_ref(ref_name)?;
54 let ref_store = RefStore::new(layout.clone());
55 let object_store = ObjectReadSnapshot::open(layout)?;
56 let patch_ids = match ref_store.read_current_ref_state_id(&canonical_ref)? {
57 Some(ref_state_id) => {
58 let envelope = object_store
59 .read_typed(ref_state_id, ObjectType::RefState)?
60 .ok_or_else(|| {
61 PrikkError::Integrity(format!(
62 "ref {canonical_ref} names missing RefState {ref_state_id}"
63 ))
64 })?;
65 let payload = RefStatePayload::decode_canonical(
66 &envelope.canonical_payload,
67 envelope.schema_version,
68 )?;
69 if payload.kind != RefKind::Branch {
70 return Err(PrikkError::Integrity(format!(
71 "ref {canonical_ref} is under heads/ but its RefState kind is not Branch"
72 )));
73 }
74 patch_ids_reachable_from_block(&object_store, payload.target_object_id)?
75 }
76 None => Vec::new(),
77 };
78 let digest = compute_patch_set_digest(&patch_ids)?;
79
80 let mut out = Vec::new();
81 out.extend_from_slice(HAVE_LIST_MAGIC);
82 push_string_u16(&mut out, &canonical_ref)?;
83 out.extend_from_slice(&digest.0);
84 push_u64(&mut out, len_to_u64(patch_ids.len())?);
85 for patch_id in &patch_ids {
86 out.extend_from_slice(patch_id.as_bytes());
87 }
88 Ok(out)
89}
90
91pub fn decode_have_list(
95 bytes: &[u8],
96 max_total_bytes: usize,
97 max_patch_count: usize,
98) -> Result<HaveList> {
99 if bytes.len() > max_total_bytes {
100 return Err(PrikkError::MalformedData(format!(
101 "have-list is {} bytes, over the configured limit of {max_total_bytes} bytes",
102 bytes.len()
103 )));
104 }
105 let mut cursor = ByteCursor::new(bytes);
106 let magic = cursor.read_array::<8>()?;
107 if &magic != HAVE_LIST_MAGIC {
108 return Err(PrikkError::MalformedData(
109 "invalid have-list magic".to_string(),
110 ));
111 }
112 let ref_name = cursor.read_string_u16()?;
113 let declared_digest = PatchSetDigest(cursor.read_array::<32>()?);
114 let patch_count = cursor.read_u64()?;
115 if patch_count > len_to_u64(max_patch_count)? {
116 return Err(PrikkError::MalformedData(format!(
117 "have-list declares {patch_count} patch ids, over the configured limit of \
118 {max_patch_count}"
119 )));
120 }
121 let mut patch_ids = Vec::new();
122 for _ in 0..patch_count {
123 patch_ids.push(ObjectId::from_bytes(cursor.read_array::<32>()?));
124 }
125 if !cursor.is_finished() {
126 return Err(PrikkError::MalformedData(
127 "trailing bytes in have-list".to_string(),
128 ));
129 }
130
131 let recomputed_digest = compute_patch_set_digest(&patch_ids)?;
135 if recomputed_digest != declared_digest {
136 return Err(PrikkError::Integrity(
137 "have-list's declared patch-set digest does not match its own carried list".to_string(),
138 ));
139 }
140
141 Ok(HaveList {
142 ref_name,
143 digest: declared_digest,
144 patch_ids,
145 })
146}
147
148#[cfg(test)]
149mod tests;