prikk_store/sync_negotiation/
summary.rs1use std::collections::{BTreeMap, BTreeSet};
15
16use prikk_error::{PrikkError, Result};
17use prikk_object::{ObjectType, RefKind, RefStatePayload};
18
19use crate::byte_cursor::ByteCursor;
20use crate::file_codec::{push_string_u16, push_u64};
21use crate::fsutil::len_to_u64;
22use crate::layout::RepositoryLayout;
23use crate::object_store::{ObjectReadSnapshot, ObjectReader};
24use crate::patch_set_digest::{
25 PatchSetDigest, compute_patch_set_digest, compute_patch_set_digest_for_ref,
26 patch_ids_reachable_from_block,
27};
28use crate::refs::RefStore;
29
30const SYNC_SUMMARY_MAGIC: &[u8; 8] = b"PSYNCSU1";
31
32pub const DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT: usize = 100_000;
34
35pub const DEFAULT_SYNC_SUMMARY_MAX_TOTAL_BYTES: usize = 16 * 1024 * 1024;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct SyncSummaryRefEntry {
41 pub ref_name: String,
43 pub digest: PatchSetDigest,
45 pub patch_count: u64,
48}
49
50pub fn build_sync_summary(layout: &RepositoryLayout) -> Result<Vec<u8>> {
55 let ref_store = RefStore::new(layout.clone());
56 let object_store = ObjectReadSnapshot::open(layout)?;
57 let mut entries: Vec<(String, PatchSetDigest, u64)> = Vec::new();
58 for pointer in ref_store.list_ref_pointers()? {
59 if !pointer.ref_name.starts_with("heads/") {
60 continue;
61 }
62 let envelope = object_store
63 .read_typed(pointer.ref_state_id, ObjectType::RefState)?
64 .ok_or_else(|| {
65 PrikkError::Integrity(format!(
66 "ref {} names missing RefState {}",
67 pointer.ref_name, pointer.ref_state_id
68 ))
69 })?;
70 let payload = RefStatePayload::decode_canonical(
71 &envelope.canonical_payload,
72 envelope.schema_version,
73 )?;
74 if payload.kind != RefKind::Branch {
75 return Err(PrikkError::Integrity(format!(
76 "ref {} is under heads/ but its RefState kind is not Branch",
77 pointer.ref_name
78 )));
79 }
80 let patch_ids = patch_ids_reachable_from_block(&object_store, payload.target_object_id)?;
81 let digest = compute_patch_set_digest(&patch_ids)?;
82 let patch_count = len_to_u64(patch_ids.len())?;
83 entries.push((pointer.ref_name, digest, patch_count));
84 }
85
86 let mut out = Vec::new();
87 out.extend_from_slice(SYNC_SUMMARY_MAGIC);
88 push_u64(&mut out, len_to_u64(entries.len())?);
89 for (ref_name, digest, patch_count) in &entries {
90 push_string_u16(&mut out, ref_name)?;
91 out.extend_from_slice(&digest.0);
92 push_u64(&mut out, *patch_count);
93 }
94 Ok(out)
95}
96
97pub fn decode_sync_summary(
102 bytes: &[u8],
103 max_total_bytes: usize,
104 max_ref_count: usize,
105) -> Result<Vec<SyncSummaryRefEntry>> {
106 if bytes.len() > max_total_bytes {
107 return Err(PrikkError::MalformedData(format!(
108 "sync summary is {} bytes, over the configured limit of {max_total_bytes} bytes",
109 bytes.len()
110 )));
111 }
112 let mut cursor = ByteCursor::new(bytes);
113 let magic = cursor.read_array::<8>()?;
114 if &magic != SYNC_SUMMARY_MAGIC {
115 return Err(PrikkError::MalformedData(
116 "invalid sync summary magic".to_string(),
117 ));
118 }
119 let ref_count = cursor.read_u64()?;
120 if ref_count > len_to_u64(max_ref_count)? {
121 return Err(PrikkError::MalformedData(format!(
122 "sync summary declares {ref_count} refs, over the configured limit of {max_ref_count}"
123 )));
124 }
125 let mut entries = Vec::new();
126 for _ in 0..ref_count {
127 let ref_name = cursor.read_string_u16()?;
128 let digest = PatchSetDigest(cursor.read_array::<32>()?);
129 let patch_count = cursor.read_u64()?;
130 entries.push(SyncSummaryRefEntry {
131 ref_name,
132 digest,
133 patch_count,
134 });
135 }
136 if !cursor.is_finished() {
137 return Err(PrikkError::MalformedData(
138 "trailing bytes in sync summary".to_string(),
139 ));
140 }
141 Ok(entries)
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum SyncRefComparisonState {
147 InSync,
149 Differs,
151 RemoteOnly,
153 LocalOnly,
155}
156
157impl SyncRefComparisonState {
158 #[must_use]
161 pub const fn as_str(self) -> &'static str {
162 match self {
163 Self::InSync => "in-sync",
164 Self::Differs => "differs",
165 Self::RemoteOnly => "remote-only",
166 Self::LocalOnly => "local-only",
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct SyncRefComparison {
174 pub ref_name: String,
176 pub state: SyncRefComparisonState,
178}
179
180pub fn compare_sync_summary(
193 layout: &RepositoryLayout,
194 remote: &[SyncSummaryRefEntry],
195) -> Result<Vec<SyncRefComparison>> {
196 let ref_store = RefStore::new(layout.clone());
197 let mut local_ref_names: BTreeSet<String> = BTreeSet::new();
198 for pointer in ref_store.list_ref_pointers()? {
199 if pointer.ref_name.starts_with("heads/") {
200 local_ref_names.insert(pointer.ref_name);
201 }
202 }
203 let remote_digests: BTreeMap<&str, PatchSetDigest> = remote
204 .iter()
205 .map(|entry| (entry.ref_name.as_str(), entry.digest))
206 .collect();
207
208 let mut all_ref_names: BTreeSet<&str> = local_ref_names.iter().map(String::as_str).collect();
209 all_ref_names.extend(remote_digests.keys().copied());
210
211 let mut comparisons = Vec::with_capacity(all_ref_names.len());
212 for ref_name in all_ref_names {
213 let is_local = local_ref_names.contains(ref_name);
214 let state = match (is_local, remote_digests.get(ref_name)) {
215 (true, Some(remote_digest)) => {
216 let local_digest = compute_patch_set_digest_for_ref(layout, ref_name)?;
217 if &local_digest == remote_digest {
218 SyncRefComparisonState::InSync
219 } else {
220 SyncRefComparisonState::Differs
221 }
222 }
223 (true, None) => SyncRefComparisonState::LocalOnly,
224 (false, Some(_)) => SyncRefComparisonState::RemoteOnly,
225 (false, None) => unreachable!(
226 "ref_name is drawn from local_ref_names or remote_digests, so at least one holds it"
227 ),
228 };
229 comparisons.push(SyncRefComparison {
230 ref_name: ref_name.to_string(),
231 state,
232 });
233 }
234 Ok(comparisons)
235}
236
237#[cfg(test)]
238mod tests;