1use std::collections::BTreeMap;
6use std::path::PathBuf;
7
8use prikk_error::{PrikkError, Result};
9use prikk_object::{
10 ObjectEnvelope, ObjectId, RefKind, RefStatePayload, RefUpdatePayload, TagPayload,
11};
12
13use crate::layout::{ContainerSlot, RepositoryLayout, ref_name_key_bytes};
14use crate::object_store::ObjectReader;
15use crate::refs::container::{
16 RefContainerRecordStatus, RefLogRecordStatus, RefLogReplay, decode_ref_container_records,
17 replay_ref_subsequence,
18};
19use crate::refs::pointer_index::{
20 PointerIndexEntry, PointerIndexRecordStatus, replay_pointer_index,
21};
22use prikk_object::ObjectType;
23
24#[derive(Debug, Clone)]
25pub(super) struct PointerState {
26 pub(super) id: ObjectId,
27 pub(super) payload: RefStatePayload,
28}
29
30#[derive(Debug, Clone)]
31pub(super) struct LogState {
32 pub(super) tip: Option<ObjectId>,
33 pub(super) previous_tip: Option<ObjectId>,
34 pub(super) record_count: usize,
35 pub(super) trailing_partial_bytes: usize,
36 pub(super) has_legacy_timestamp: bool,
37}
38
39pub(super) struct RefLogEnvelope {
40 pub(super) ref_name: String,
41 pub(super) sequence: u64,
42 pub(super) envelope: ObjectEnvelope,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum RefFileStatus {
51 Evaluated {
53 ref_name: String,
55 },
56 Failed {
58 message: String,
60 },
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct RefFileOutcome {
66 pub path: PathBuf,
68 pub status: RefFileStatus,
70}
71
72fn pointer_locator(layout: &RepositoryLayout, offset: usize) -> PathBuf {
73 layout
77 .ref_pointer_index_slot_path(ContainerSlot::A)
78 .join(format!("#{offset}"))
79}
80
81fn log_locator(layout: &RepositoryLayout, offset: usize) -> PathBuf {
82 layout
83 .ref_log_container_slot_path(ContainerSlot::A)
84 .join(format!("#{offset}"))
85}
86
87#[allow(clippy::type_complexity)]
96pub(super) fn read_pointers(
97 layout: &RepositoryLayout,
98 objects: &impl ObjectReader,
99) -> Result<(
100 BTreeMap<String, PointerState>,
101 BTreeMap<[u8; 32], String>,
102 Vec<RefFileOutcome>,
103)> {
104 let replay = replay_pointer_index(layout)?;
105 if replay.has_item_failure() {
106 return Err(PrikkError::Integrity(
107 "ref pointer index has a damaged entry; run doctor before verify can classify \
108 publication state"
109 .to_string(),
110 ));
111 }
112 let mut latest: BTreeMap<[u8; 32], (usize, PointerIndexEntry)> = BTreeMap::new();
116 for outcome in replay.record_outcomes.iter().zip(replay.entries) {
117 let (record_outcome, entry) = outcome;
118 if !matches!(record_outcome.status, PointerIndexRecordStatus::Evaluated) {
119 continue;
120 }
121 latest.insert(entry.ref_name_key, (record_outcome.offset, entry));
122 }
123
124 let mut pointers = BTreeMap::new();
125 let mut failures_by_key = BTreeMap::new();
126 let mut outcomes = Vec::new();
127 for (key, (offset, entry)) in latest {
128 let locator = pointer_locator(layout, offset);
129 match read_one_pointer_entry(objects, &entry) {
130 Ok((ref_name, state)) => {
131 pointers.insert(ref_name.clone(), state);
132 outcomes.push(RefFileOutcome {
133 path: locator,
134 status: RefFileStatus::Evaluated { ref_name },
135 });
136 }
137 Err(err) => {
138 failures_by_key.insert(key, err.to_string());
139 outcomes.push(RefFileOutcome {
140 path: locator,
141 status: RefFileStatus::Failed {
142 message: err.to_string(),
143 },
144 });
145 }
146 }
147 }
148 Ok((pointers, failures_by_key, outcomes))
149}
150
151fn read_one_pointer_entry(
152 objects: &impl ObjectReader,
153 entry: &PointerIndexEntry,
154) -> Result<(String, PointerState)> {
155 if ref_name_key_bytes(&entry.ref_name) != entry.ref_name_key {
159 return Err(PrikkError::Integrity(format!(
160 "pointer index entry ref_name_key does not match sha256({})",
161 entry.ref_name
162 )));
163 }
164 let payload = verified_ref_state_payload(objects, entry.ref_state_id)?;
165 if payload.ref_name != entry.ref_name {
166 return Err(PrikkError::Integrity(format!(
167 "RefState {} name differs from pointer ref {}",
168 entry.ref_state_id, entry.ref_name
169 )));
170 }
171 ensure_ref_target_valid(
172 objects,
173 payload.kind,
174 payload.target_object_id,
175 entry.ref_state_id,
176 )?;
177 Ok((
178 entry.ref_name.clone(),
179 PointerState {
180 id: entry.ref_state_id,
181 payload,
182 },
183 ))
184}
185
186#[allow(clippy::type_complexity)]
192pub(super) fn read_logs(
193 layout: &RepositoryLayout,
194 objects: &impl ObjectReader,
195 _pointers: &BTreeMap<String, PointerState>,
196) -> Result<(
197 BTreeMap<String, LogState>,
198 usize,
199 Vec<RefLogEnvelope>,
200 BTreeMap<[u8; 32], String>,
201 Vec<RefFileOutcome>,
202)> {
203 let relative =
204 layout.repository_relative(&layout.ref_log_container_slot_path(ContainerSlot::A))?;
205 let Some(bytes) =
206 crate::fsutil::read_file_if_exists(layout.repository_mutation_root(), &relative)?
207 else {
208 return Ok((BTreeMap::new(), 0, Vec::new(), BTreeMap::new(), Vec::new()));
209 };
210 let discovery = decode_ref_container_records(&bytes)?;
211 let mut keys: std::collections::BTreeSet<[u8; 32]> = discovery
212 .records
213 .iter()
214 .map(|record| record.ref_name_key)
215 .collect();
216 keys.extend(
217 discovery
218 .record_outcomes
219 .iter()
220 .filter_map(|outcome| match &outcome.status {
221 RefContainerRecordStatus::Failed {
222 claimed_ref_name_key: Some(key),
223 ..
224 } => Some(*key),
225 _ => None,
226 }),
227 );
228
229 let mut logs = BTreeMap::new();
230 let mut total = 0_usize;
231 let mut envelopes = Vec::new();
232 let mut failures_by_key = BTreeMap::new();
233 let mut outcomes = Vec::new();
234 for key in keys {
235 let replay = replay_ref_subsequence(layout, key)?;
236 let first_offset = replay
237 .record_outcomes
238 .first()
239 .map_or(0, |outcome| outcome.offset);
240 if replay.has_item_failure() {
244 let joined = replay
245 .record_outcomes
246 .iter()
247 .filter_map(|outcome| match &outcome.status {
248 RefLogRecordStatus::Failed { message } => {
249 Some(format!("offset {}: {message}", outcome.offset))
250 }
251 RefLogRecordStatus::Evaluated => None,
252 })
253 .collect::<Vec<_>>()
254 .join("; ");
255 let message = format!("ref log has damaged record(s): {joined}");
256 failures_by_key.insert(key, message.clone());
257 outcomes.push(RefFileOutcome {
258 path: log_locator(layout, first_offset),
259 status: RefFileStatus::Failed { message },
260 });
261 continue;
262 }
263 match validate_log_replay(objects, key, &replay) {
264 Ok(Some((ref_name, state, record_envelopes))) => {
265 total = match total.checked_add(record_envelopes.len()) {
266 Some(value) => value,
267 None => {
268 return Err(PrikkError::Integrity("ref-log count overflow".to_string()));
269 }
270 };
271 envelopes.extend(record_envelopes);
272 logs.insert(ref_name.clone(), state);
273 outcomes.push(RefFileOutcome {
274 path: log_locator(layout, first_offset),
275 status: RefFileStatus::Evaluated { ref_name },
276 });
277 }
278 Ok(None) => {}
279 Err(err) => {
280 failures_by_key.insert(key, err.to_string());
281 outcomes.push(RefFileOutcome {
282 path: log_locator(layout, first_offset),
283 status: RefFileStatus::Failed {
284 message: err.to_string(),
285 },
286 });
287 }
288 }
289 }
290 Ok((logs, total, envelopes, failures_by_key, outcomes))
291}
292
293#[allow(clippy::type_complexity)]
294fn validate_log_replay(
295 objects: &impl ObjectReader,
296 ref_name_key: [u8; 32],
297 replay: &RefLogReplay,
298) -> Result<Option<(String, LogState, Vec<RefLogEnvelope>)>> {
299 if replay.records.is_empty() {
300 if replay.trailing_partial_bytes == 0 {
301 return Ok(None);
302 }
303 return Err(PrikkError::Integrity(
304 "ref log has a trailing partial record with no sound records of its own".to_string(),
305 ));
306 }
307 let mut previous = None;
308 let mut previous_tip = None;
309 let mut ref_name = None;
310 let mut has_legacy_timestamp = false;
311 let mut record_envelopes = Vec::with_capacity(replay.records.len());
312 for (index, record) in replay.records.iter().enumerate() {
313 let update = RefUpdatePayload::decode_canonical(&record.envelope.canonical_payload)?;
314 if crate::layout::ref_name_key_bytes(&update.ref_name) != ref_name_key {
318 return Err(PrikkError::Integrity(
319 "ref container record header ref_name_key does not match its own envelope"
320 .to_string(),
321 ));
322 }
323 let expected_seq = u64::try_from(index)
324 .ok()
325 .and_then(|value| value.checked_add(1))
326 .ok_or_else(|| PrikkError::Integrity("ref-log sequence overflow".to_string()))?;
327 if ref_name
328 .as_ref()
329 .is_some_and(|name| name != &update.ref_name)
330 || update.old_ref_state_id != previous
331 || update.update_seq != expected_seq
332 {
333 return Err(PrikkError::Integrity(format!(
334 "ref-log chain or sequence diverges for {}",
335 update.ref_name
336 )));
337 }
338 verify_update(objects, &update)?;
339 has_legacy_timestamp |= update.created_at != 0;
340 ref_name.get_or_insert_with(|| update.ref_name.clone());
341 previous_tip = previous;
342 previous = Some(update.new_ref_state_id);
343 record_envelopes.push(RefLogEnvelope {
344 ref_name: update.ref_name.clone(),
345 sequence: update.update_seq,
346 envelope: record.envelope.clone(),
347 });
348 }
349 let name = ref_name
350 .ok_or_else(|| PrikkError::Integrity("non-empty ref log has no identity".to_string()))?;
351 Ok(Some((
352 name,
353 LogState {
354 tip: previous,
355 previous_tip,
356 record_count: replay.records.len(),
357 trailing_partial_bytes: replay.trailing_partial_bytes,
358 has_legacy_timestamp,
359 },
360 record_envelopes,
361 )))
362}
363
364fn verify_update(objects: &impl ObjectReader, update: &RefUpdatePayload) -> Result<()> {
365 let state = verified_ref_state_payload(objects, update.new_ref_state_id)?;
366 if state.ref_name != update.ref_name
367 || state.previous_ref_state_id != update.old_ref_state_id
368 || state.target_object_id != update.new_target_object_id
369 || state.update_seq != update.update_seq
370 {
371 return Err(PrikkError::Integrity(format!(
372 "RefState disagrees with RefUpdate for {}",
373 update.ref_name
374 )));
375 }
376 ensure_ref_target_valid(
377 objects,
378 state.kind,
379 update.new_target_object_id,
380 update.new_ref_state_id,
381 )
382}
383
384fn verified_ref_state_payload(
385 objects: &impl ObjectReader,
386 ref_state_id: ObjectId,
387) -> Result<RefStatePayload> {
388 let envelope = objects
389 .read_typed(ref_state_id, ObjectType::RefState)?
390 .ok_or_else(|| PrikkError::Integrity(format!("missing RefState object: {ref_state_id}")))?;
391 if envelope.signatures.is_empty() {
392 return Err(PrikkError::Integrity(format!(
393 "RefState {ref_state_id} is unsigned"
394 )));
395 }
396 RefStatePayload::decode_canonical(&envelope.canonical_payload, envelope.schema_version)
397}
398
399pub(crate) fn ensure_ref_target_valid(
415 objects: &impl ObjectReader,
416 kind: RefKind,
417 target_object_id: ObjectId,
418 owner: ObjectId,
419) -> Result<()> {
420 match kind {
421 RefKind::Branch => ensure_block_exists(objects, target_object_id, owner),
422 RefKind::Tag => {
423 let tag_envelope = objects
424 .read_typed(target_object_id, ObjectType::Tag)?
425 .ok_or_else(|| {
426 PrikkError::Integrity(format!(
427 "ref object {owner} targets missing tag {target_object_id}"
428 ))
429 })?;
430 let tag_payload = TagPayload::decode_canonical(&tag_envelope.canonical_payload)?;
431 ensure_block_exists(objects, tag_payload.target_block_id, owner)
432 }
433 }
434}
435
436fn ensure_block_exists(
437 objects: &impl ObjectReader,
438 block_id: ObjectId,
439 owner: ObjectId,
440) -> Result<()> {
441 if objects.read_typed(block_id, ObjectType::Block)?.is_some() {
442 return Ok(());
443 }
444 Err(PrikkError::Integrity(format!(
445 "ref object {owner} targets missing block {block_id}"
446 )))
447}