1use std::collections::HashSet;
6use std::path::{Path, PathBuf};
7
8use prikk_error::{PrikkError, Result};
9use prikk_object::{BlockPayload, ObjectEnvelope, ObjectId, ObjectType};
10
11use super::{
12 AuthorSignatureVerification, BlockSealVerification, ObjectVerification,
13 PublicationTrustVerifier, verify_block_payload,
14};
15use crate::block_state::{BlockStateOutcome, LineageStateMemo, verify_blocks_topological};
16use crate::container::{self, ContainerRecordStatus};
17use crate::fsutil::{EntryKind, inspect_entry, list_directory, read_file_if_exists};
18use crate::index::replay_index;
19use crate::layout::{ContainerSlot, RepositoryLayout, persisted_object_types};
20use crate::object_store::ObjectReader;
21use crate::signature_diagnostics::{
22 SignatureEnvelopeIssue, SignatureEnvelopeSource, classify_signature_envelope,
23};
24
25#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum ObjectItemStatus {
31 Evaluated(ObjectVerification),
33 Unindexed(ObjectVerification),
38 Failed {
44 message: String,
46 },
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ObjectItemOutcome {
52 pub object_type: ObjectType,
54 pub path: PathBuf,
59 pub status: ObjectItemStatus,
61}
62
63pub(super) struct ObjectSummary {
64 pub(super) item_outcomes: Vec<ObjectItemOutcome>,
67 pub(super) topological_outcomes: Vec<BlockStateOutcome>,
71 pub(super) temp_paths: Vec<PathBuf>,
72 pub(super) signature_issues: Vec<SignatureEnvelopeIssue>,
73 pub(super) merge_baseline_divergences: Vec<super::MergeBaselineDivergence>,
74 pub(super) block_seals: Vec<BlockSealVerification>,
75}
76
77impl ObjectSummary {
78 fn empty() -> Self {
79 Self {
80 item_outcomes: Vec::new(),
81 topological_outcomes: Vec::new(),
82 temp_paths: Vec::new(),
83 signature_issues: Vec::new(),
84 merge_baseline_divergences: Vec::new(),
85 block_seals: Vec::new(),
86 }
87 }
88
89 fn add(&mut self, other: Self) {
90 self.item_outcomes.extend(other.item_outcomes);
91 self.topological_outcomes.extend(other.topological_outcomes);
92 self.temp_paths.extend(other.temp_paths);
93 self.signature_issues.extend(other.signature_issues);
94 self.merge_baseline_divergences
95 .extend(other.merge_baseline_divergences);
96 self.block_seals.extend(other.block_seals);
97 }
98}
99
100pub(super) fn verify_objects(
101 layout: &RepositoryLayout,
102 object_store: &impl ObjectReader,
103 trust_verifier: &mut PublicationTrustVerifier<'_>,
104) -> Result<ObjectSummary> {
105 let mut lineage_memo = LineageStateMemo::new();
114 let mut pending_v3_blocks: Vec<(ObjectId, BlockPayload)> = Vec::new();
115 let mut summary = ObjectSummary::empty();
116
117 let index_replay = replay_index(layout)?;
124 if index_replay.has_item_failure() {
125 return Err(PrikkError::Integrity(
126 "object index has a damaged entry; run doctor before verify can classify indexing"
127 .to_string(),
128 ));
129 }
130 let indexed_ids: HashSet<ObjectId> = index_replay
131 .entries
132 .iter()
133 .map(|entry| entry.object_id)
134 .collect();
135
136 for entry in &index_replay.entries {
151 let Ok(envelope) = crate::index::read_object_envelope_at(layout, entry) else {
152 continue;
153 };
154 let computed = envelope.object_id();
155 if computed != entry.object_id {
156 return Err(PrikkError::Integrity(format!(
157 "index entry for {} resolves to an envelope with computed id {computed}",
158 entry.object_id
159 )));
160 }
161 }
162
163 for object_type in persisted_object_types() {
164 summary.add(verify_object_type_container(
165 layout,
166 object_store,
167 object_type,
168 trust_verifier,
169 &mut pending_v3_blocks,
170 &indexed_ids,
171 )?);
172 }
173 summary.temp_paths = scan_loose_file_temp_debris(layout)?;
174
175 let topological =
176 verify_blocks_topological(object_store, &pending_v3_blocks, &mut lineage_memo)?;
177 summary.topological_outcomes = topological.outcomes;
178 Ok(summary)
179}
180
181fn verify_object_type_container(
182 layout: &RepositoryLayout,
183 object_store: &impl ObjectReader,
184 object_type: ObjectType,
185 trust_verifier: &mut PublicationTrustVerifier<'_>,
186 pending_v3_blocks: &mut Vec<(ObjectId, BlockPayload)>,
187 indexed_ids: &HashSet<ObjectId>,
188) -> Result<ObjectSummary> {
189 let mut summary = ObjectSummary::empty();
190 let container_path = layout.container_slot_path(object_type, ContainerSlot::A);
191 let relative = layout.repository_relative(&container_path)?;
192 let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
193 return Ok(summary);
197 };
198 let replay = container::decode_container_records(object_type, &bytes)?;
199
200 let mut records = replay.records.into_iter();
203 for outcome in &replay.record_outcomes {
204 let locator = container_path.join(format!("#{}", outcome.offset));
205 let ContainerRecordStatus::Evaluated { .. } = &outcome.status else {
206 let ContainerRecordStatus::Failed { message } = &outcome.status else {
207 unreachable!("record_outcomes only ever holds Evaluated or Failed")
208 };
209 summary.item_outcomes.push(ObjectItemOutcome {
210 object_type,
211 path: locator,
212 status: ObjectItemStatus::Failed {
213 message: message.clone(),
214 },
215 });
216 continue;
217 };
218 let Some(record) = records.next() else {
219 return Err(PrikkError::Integrity(
220 "container replay outcome/record count mismatch".to_string(),
221 ));
222 };
223 match verify_object_record(
227 layout,
228 object_store,
229 object_type,
230 &locator,
231 &record.envelope,
232 trust_verifier,
233 pending_v3_blocks,
234 ) {
235 Ok((object, signature_issues, merge_baseline_divergence)) => {
236 summary.signature_issues.extend(signature_issues);
237 summary
238 .merge_baseline_divergences
239 .extend(merge_baseline_divergence);
240 if object.object_type == ObjectType::Block {
241 if let Some(sealed_by_key_id) = object.sealed_by_key_id.clone() {
242 summary.block_seals.push(BlockSealVerification {
243 block_id: object.object_id,
244 sealed_by_key_id,
245 });
246 }
247 }
248 let status = if indexed_ids.contains(&object.object_id) {
249 ObjectItemStatus::Evaluated(object)
250 } else {
251 ObjectItemStatus::Unindexed(object)
252 };
253 summary.item_outcomes.push(ObjectItemOutcome {
254 object_type,
255 path: locator,
256 status,
257 });
258 }
259 Err(err) => {
260 summary.item_outcomes.push(ObjectItemOutcome {
261 object_type,
262 path: locator,
263 status: ObjectItemStatus::Failed {
264 message: err.to_string(),
265 },
266 });
267 }
268 }
269 }
270 Ok(summary)
271}
272
273fn verify_object_record(
274 layout: &RepositoryLayout,
275 object_store: &impl ObjectReader,
276 object_type: ObjectType,
277 locator: &Path,
278 envelope: &ObjectEnvelope,
279 trust_verifier: &mut PublicationTrustVerifier<'_>,
280 pending_v3_blocks: &mut Vec<(ObjectId, BlockPayload)>,
281) -> Result<(
282 ObjectVerification,
283 Vec<SignatureEnvelopeIssue>,
284 Option<super::MergeBaselineDivergence>,
285)> {
286 crate::format::validate_read_schema(layout.format(), envelope)?;
291 let object_id = envelope.object_id();
292 let signature_issues = classify_signature_envelope(
293 envelope,
294 SignatureEnvelopeSource::Object {
295 object_type,
296 object_id,
297 },
298 )?;
299 let sealed_by_key_id = if matches!(object_type, ObjectType::Block | ObjectType::RefState) {
300 trust_verifier.verify(envelope)?
301 } else {
302 None
303 };
304 let author_verification = if object_type == ObjectType::Patch {
309 crate::author_key_index::verify_author_signature(layout, envelope)?.map(
310 |(key_id, sound)| {
311 if sound {
312 AuthorSignatureVerification::Sound { key_id }
313 } else {
314 AuthorSignatureVerification::Unverifiable { key_id }
315 }
316 },
317 )
318 } else {
319 None
320 };
321 let (rollback_patch_count, merge_baseline_divergence) = if object_type == ObjectType::Block {
322 verify_block_payload(
323 object_store,
324 object_id,
325 layout.format(),
326 &envelope.canonical_payload,
327 pending_v3_blocks,
328 )?
329 } else {
330 (0, None)
331 };
332 Ok((
333 ObjectVerification {
334 object_id,
335 object_type,
336 path: locator.to_path_buf(),
337 rollback_patch_count,
338 sealed_by_key_id,
339 author_verification,
340 },
341 signature_issues,
342 merge_baseline_divergence,
343 ))
344}
345
346fn scan_loose_file_temp_debris(layout: &RepositoryLayout) -> Result<Vec<PathBuf>> {
354 let mut temp_paths = Vec::new();
355 for object_type in persisted_object_types() {
356 let dir = layout.object_type_dir(object_type);
357 let relative_dir = layout.repository_relative(&dir)?;
358 match inspect_entry(layout.repository_mutation_root(), &relative_dir)? {
359 None => continue,
360 Some(EntryKind::Directory) => {}
361 Some(_) => {
362 return Err(PrikkError::Integrity(format!(
363 "unexpected non-directory in object type directory: {}",
364 dir.display()
365 )));
366 }
367 }
368 let mut prefix_entries = list_directory(layout.repository_mutation_root(), &relative_dir)?;
369 prefix_entries.sort_by(|left, right| {
370 left.name
371 .as_encoded_bytes()
372 .cmp(right.name.as_encoded_bytes())
373 });
374 for prefix_entry in prefix_entries {
375 let prefix_path = dir.join(&prefix_entry.name);
376 if prefix_entry.kind != EntryKind::Directory {
377 return Err(PrikkError::Integrity(format!(
378 "unexpected non-directory in object type directory: {}",
379 prefix_path.display()
380 )));
381 }
382 let relative_prefix = layout.repository_relative(&prefix_path)?;
383 let mut entries = list_directory(layout.repository_mutation_root(), &relative_prefix)?;
384 entries.sort_by(|left, right| {
385 left.name
386 .as_encoded_bytes()
387 .cmp(right.name.as_encoded_bytes())
388 });
389 for entry in entries {
390 let path = prefix_path.join(&entry.name);
391 if entry.kind != EntryKind::Regular {
392 return Err(PrikkError::Integrity(format!(
393 "unexpected non-file in object prefix directory: {}",
394 path.display()
395 )));
396 }
397 if is_object_temp_path(&path) {
398 temp_paths.push(path);
399 continue;
400 }
401 return Err(PrikkError::Integrity(format!(
402 "unexpected loose object file under format-3 (containers own object storage \
403 now): {}",
404 path.display()
405 )));
406 }
407 }
408 }
409 Ok(temp_paths)
410}
411
412fn is_object_temp_path(path: &Path) -> bool {
413 let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
414 return false;
415 };
416 let Some((object_name, suffix)) = name.split_once(".pobj.tmp.") else {
417 return false;
418 };
419 let Some((pid, random)) = suffix.split_once('.') else {
420 return false;
421 };
422 object_name.len() == 64
423 && object_name.bytes().all(|byte| byte.is_ascii_hexdigit())
424 && !pid.is_empty()
425 && pid.bytes().all(|byte| byte.is_ascii_digit())
426 && random.len() == 32
427 && random.bytes().all(|byte| byte.is_ascii_hexdigit())
428}