1use std::collections::{BTreeMap, BTreeSet};
31
32use super::{
33 build::{ManifestNodeSource, ManifestNodeStore, build_manifest},
34 extent::PackRangeClaim,
35 node::{
36 MANIFEST_LEAF_MAX_ENTRIES, MANIFEST_ROUTE_LEVELS, ManifestDecodeError, ManifestKey,
37 ManifestNode, ManifestObject,
38 },
39};
40use crate::object::ContentHash;
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub enum FsckRule {
47 MalformedNode,
51 NonCanonicalNodeEncoding,
53 TrailingBytes,
55 LeafEntriesOutOfOrder,
57 DuplicateObjectKey,
59 EmptyBranchBitmap,
61
62 NodeDigestMismatch,
65 SubtreeSummaryMismatch,
68 ObjectSizeMismatch,
70 ExtentDigestMismatch,
72
73 DanglingNodeRef,
76 DanglingObjectRef,
78 ExtentObjectNotInManifest,
80 UnreachableNode,
82
83 LeafOverfull,
86 UnderfullBranch,
88 EmptyNonRootLeaf,
90 BranchDepthMismatch,
92 MisroutedEntry,
94 NonCanonicalTrieShape,
97 DepthExceeded,
99
100 ExtentsOutOfOffsetOrder,
103 ExtentOverlap,
105 ExtentGap,
107 RangeCoverageMismatch,
109 ZeroLengthExtent,
111}
112
113impl FsckRule {
114 pub fn name(self) -> &'static str {
116 match self {
117 Self::MalformedNode => "malformed-node",
118 Self::NonCanonicalNodeEncoding => "non-canonical-node-encoding",
119 Self::TrailingBytes => "trailing-bytes",
120 Self::LeafEntriesOutOfOrder => "leaf-entries-out-of-order",
121 Self::DuplicateObjectKey => "duplicate-object-key",
122 Self::EmptyBranchBitmap => "empty-branch-bitmap",
123 Self::NodeDigestMismatch => "node-digest-mismatch",
124 Self::SubtreeSummaryMismatch => "subtree-summary-mismatch",
125 Self::ObjectSizeMismatch => "object-size-mismatch",
126 Self::ExtentDigestMismatch => "extent-digest-mismatch",
127 Self::DanglingNodeRef => "dangling-node-ref",
128 Self::DanglingObjectRef => "dangling-object-ref",
129 Self::ExtentObjectNotInManifest => "extent-object-not-in-manifest",
130 Self::UnreachableNode => "unreachable-node",
131 Self::LeafOverfull => "leaf-overfull",
132 Self::UnderfullBranch => "underfull-branch",
133 Self::EmptyNonRootLeaf => "empty-non-root-leaf",
134 Self::BranchDepthMismatch => "branch-depth-mismatch",
135 Self::MisroutedEntry => "misrouted-entry",
136 Self::NonCanonicalTrieShape => "non-canonical-trie-shape",
137 Self::DepthExceeded => "depth-exceeded",
138 Self::ExtentsOutOfOffsetOrder => "extents-out-of-offset-order",
139 Self::ExtentOverlap => "extent-overlap",
140 Self::ExtentGap => "extent-gap",
141 Self::RangeCoverageMismatch => "range-coverage-mismatch",
142 Self::ZeroLengthExtent => "zero-length-extent",
143 }
144 }
145}
146
147impl std::fmt::Display for FsckRule {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.write_str(self.name())
150 }
151}
152
153impl ManifestDecodeError {
154 pub fn fsck_rule(&self) -> FsckRule {
157 match self {
158 Self::BadMagic
159 | Self::UnsupportedVersion(_)
160 | Self::UnknownNodeTag(_)
161 | Self::UnknownObjectKind(_)
162 | Self::Truncated => FsckRule::MalformedNode,
163 Self::TrailingBytes => FsckRule::TrailingBytes,
164 Self::EntriesOutOfOrder => FsckRule::LeafEntriesOutOfOrder,
165 Self::DuplicateObjectKey(_) => FsckRule::DuplicateObjectKey,
166 Self::EmptyBranchBitmap => FsckRule::EmptyBranchBitmap,
167 Self::NonCanonicalEncoding => FsckRule::NonCanonicalNodeEncoding,
168 Self::AddressMismatch { .. } => FsckRule::NodeDigestMismatch,
169 }
170 }
171}
172
173#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct FsckFinding {
178 pub rule: FsckRule,
179 pub node: Option<ContentHash>,
181 pub detail: String,
182}
183
184impl FsckFinding {
185 fn new(rule: FsckRule, node: Option<ContentHash>, detail: impl Into<String>) -> Self {
186 Self {
187 rule,
188 node,
189 detail: detail.into(),
190 }
191 }
192}
193
194impl std::fmt::Display for FsckFinding {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self.node {
197 Some(node) => write!(f, "{}: {} ({})", self.rule, self.detail, node.short()),
198 None => write!(f, "{}: {}", self.rule, self.detail),
199 }
200 }
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Eq)]
205pub struct FsckReport {
206 findings: Vec<FsckFinding>,
207}
208
209impl FsckReport {
210 pub fn findings(&self) -> &[FsckFinding] {
211 &self.findings
212 }
213
214 pub fn is_clean(&self) -> bool {
217 self.findings.is_empty()
218 }
219
220 pub fn violated_rules(&self) -> BTreeSet<FsckRule> {
222 self.findings.iter().map(|finding| finding.rule).collect()
223 }
224
225 pub fn has_rule(&self, rule: FsckRule) -> bool {
226 self.findings.iter().any(|finding| finding.rule == rule)
227 }
228
229 fn push(&mut self, finding: FsckFinding) {
230 self.findings.push(finding);
231 }
232}
233
234pub trait ManifestObjectIndex {
243 fn decoded_size(&self, key: &ManifestKey) -> Option<u64>;
245}
246
247impl ManifestObjectIndex for BTreeMap<ManifestKey, u64> {
248 fn decoded_size(&self, key: &ManifestKey) -> Option<u64> {
249 self.get(key).copied()
250 }
251}
252
253impl ManifestObjectIndex for std::collections::HashMap<ManifestKey, u64> {
254 fn decoded_size(&self, key: &ManifestKey) -> Option<u64> {
255 self.get(key).copied()
256 }
257}
258
259#[derive(Default)]
261pub struct FsckOptions<'a> {
262 pub objects: Option<&'a dyn ManifestObjectIndex>,
265 pub report_unreachable: bool,
269}
270
271pub fn fsck_manifest<S: ManifestNodeSource + ?Sized>(source: &S, root: &ContentHash) -> FsckReport {
275 fsck_manifest_with(source, root, &FsckOptions::default())
276}
277
278pub fn fsck_manifest_with<S: ManifestNodeSource + ?Sized>(
281 source: &S,
282 root: &ContentHash,
283 options: &FsckOptions<'_>,
284) -> FsckReport {
285 let mut report = FsckReport::default();
286 let mut visited = BTreeSet::new();
287 let mut objects = Vec::new();
288 let mut expansion_complete = true;
289
290 visit(
291 source,
292 root,
293 0,
294 true,
295 options,
296 &mut visited,
297 &mut objects,
298 &mut report,
299 &mut expansion_complete,
300 );
301
302 if expansion_complete
308 && let Ok(rebuilt) = build_manifest(objects.iter().copied())
309 && rebuilt.root != *root
310 {
311 report.push(FsckFinding::new(
312 FsckRule::NonCanonicalTrieShape,
313 Some(*root),
314 format!(
315 "rebuilding from {} expanded objects yields root {}",
316 objects.len(),
317 rebuilt.root
318 ),
319 ));
320 }
321
322 report
323}
324
325pub fn fsck_manifest_store<S: ManifestNodeStore + ?Sized>(
328 store: &S,
329 roots: &[ContentHash],
330 options: &FsckOptions<'_>,
331) -> FsckReport {
332 let mut report = FsckReport::default();
333 let mut reachable = BTreeSet::new();
334
335 for root in roots {
336 let mut visited = BTreeSet::new();
337 let mut objects = Vec::new();
338 let mut expansion_complete = true;
339 visit(
340 store,
341 root,
342 0,
343 true,
344 options,
345 &mut visited,
346 &mut objects,
347 &mut report,
348 &mut expansion_complete,
349 );
350 if expansion_complete
351 && let Ok(rebuilt) = build_manifest(objects.iter().copied())
352 && rebuilt.root != *root
353 {
354 report.push(FsckFinding::new(
355 FsckRule::NonCanonicalTrieShape,
356 Some(*root),
357 format!("rebuilt root {} differs", rebuilt.root),
358 ));
359 }
360 reachable.extend(visited);
361 }
362
363 if options.report_unreachable {
364 for hash in store.node_hashes() {
365 if !reachable.contains(&hash) {
366 report.push(FsckFinding::new(
367 FsckRule::UnreachableNode,
368 Some(hash),
369 "node is present but no supplied root reaches it",
370 ));
371 }
372 }
373 }
374
375 report
376}
377
378#[allow(clippy::too_many_arguments)]
386fn visit<S: ManifestNodeSource + ?Sized>(
387 source: &S,
388 hash: &ContentHash,
389 depth: u8,
390 is_root: bool,
391 options: &FsckOptions<'_>,
392 visited: &mut BTreeSet<ContentHash>,
393 objects: &mut Vec<ManifestObject>,
394 report: &mut FsckReport,
395 expansion_complete: &mut bool,
396) -> Option<(u64, u64)> {
397 if depth > MANIFEST_ROUTE_LEVELS {
398 *expansion_complete = false;
399 report.push(FsckFinding::new(
400 FsckRule::DepthExceeded,
401 Some(*hash),
402 format!("traversal passed the fixed {MANIFEST_ROUTE_LEVELS}-level route"),
403 ));
404 return None;
405 }
406 if !visited.insert(*hash) {
407 return None;
410 }
411
412 let Some(bytes) = source.node_bytes(hash) else {
413 *expansion_complete = false;
414 report.push(FsckFinding::new(
415 FsckRule::DanglingNodeRef,
416 Some(*hash),
417 "node is referenced but absent from the node source",
418 ));
419 return None;
420 };
421
422 let actual = ContentHash::compute(bytes);
423 if actual != *hash {
424 *expansion_complete = false;
425 report.push(FsckFinding::new(
426 FsckRule::NodeDigestMismatch,
427 Some(*hash),
428 format!("bytes hash to {actual}"),
429 ));
430 return None;
431 }
432
433 let node = match ManifestNode::decode(bytes) {
434 Ok(node) => node,
435 Err(error) => {
436 *expansion_complete = false;
437 report.push(FsckFinding::new(
438 error.fsck_rule(),
439 Some(*hash),
440 error.to_string(),
441 ));
442 return None;
443 }
444 };
445
446 match node {
447 ManifestNode::Leaf(leaf) => {
448 let entries = leaf.entries();
449 if entries.is_empty() && !is_root {
450 report.push(FsckFinding::new(
451 FsckRule::EmptyNonRootLeaf,
452 Some(*hash),
453 "only the root may be the empty leaf",
454 ));
455 }
456 if entries.len() > MANIFEST_LEAF_MAX_ENTRIES && depth < MANIFEST_ROUTE_LEVELS {
457 report.push(FsckFinding::new(
458 FsckRule::LeafOverfull,
459 Some(*hash),
460 format!(
461 "leaf at depth {depth} holds {} entries; the bound is {MANIFEST_LEAF_MAX_ENTRIES} while routing bits remain",
462 entries.len()
463 ),
464 ));
465 }
466
467 let mut decoded_bytes = 0u64;
468 for entry in entries {
469 decoded_bytes = decoded_bytes.saturating_add(entry.decoded_size);
470 if let Some(index) = options.objects {
471 match index.decoded_size(&entry.key()) {
472 None => {
473 report.push(FsckFinding::new(
474 FsckRule::DanglingObjectRef,
475 Some(*hash),
476 format!("{} object {} is not present", entry.kind, entry.hash),
477 ));
478 }
479 Some(size) if size != entry.decoded_size => {
480 report.push(FsckFinding::new(
481 FsckRule::ObjectSizeMismatch,
482 Some(*hash),
483 format!(
484 "{} object {} declares {} bytes but holds {size}",
485 entry.kind, entry.hash, entry.decoded_size
486 ),
487 ));
488 }
489 Some(_) => {}
490 }
491 }
492 objects.push(*entry);
493 }
494 Some((entries.len() as u64, decoded_bytes))
495 }
496 ManifestNode::Branch(branch) => {
497 if branch.depth() != depth {
498 report.push(FsckFinding::new(
499 FsckRule::BranchDepthMismatch,
500 Some(*hash),
501 format!("declares depth {} but sits at {depth}", branch.depth()),
502 ));
503 }
504
505 let mut total_count = 0u64;
506 let mut total_bytes = 0u64;
507 let mut all_children_summarized = true;
508 for child in branch.children() {
509 let before = objects.len();
510 let summary = visit(
511 source,
512 &child.hash,
513 depth + 1,
514 false,
515 options,
516 visited,
517 objects,
518 report,
519 expansion_complete,
520 );
521
522 for entry in &objects[before..] {
526 if entry.key().route().group(depth) != child.slot {
527 report.push(FsckFinding::new(
528 FsckRule::MisroutedEntry,
529 Some(child.hash),
530 format!(
531 "{} object {} routes to slot {} at depth {depth}, not {}",
532 entry.kind,
533 entry.hash,
534 entry.key().route().group(depth),
535 child.slot
536 ),
537 ));
538 }
539 }
540
541 match summary {
542 Some((count, bytes)) => {
543 if count != child.object_count || bytes != child.decoded_bytes {
544 report.push(FsckFinding::new(
545 FsckRule::SubtreeSummaryMismatch,
546 Some(*hash),
547 format!(
548 "slot {} summarizes ({}, {}) but holds ({count}, {bytes})",
549 child.slot, child.object_count, child.decoded_bytes
550 ),
551 ));
552 }
553 total_count += count;
554 total_bytes = total_bytes.saturating_add(bytes);
555 }
556 None => {
557 all_children_summarized = false;
561 }
562 }
563 }
564
565 if all_children_summarized {
566 if total_count <= MANIFEST_LEAF_MAX_ENTRIES as u64 && depth < MANIFEST_ROUTE_LEVELS
567 {
568 report.push(FsckFinding::new(
569 FsckRule::UnderfullBranch,
570 Some(*hash),
571 format!(
572 "branch subtree holds {total_count} objects; it must be a single leaf"
573 ),
574 ));
575 }
576 Some((total_count, total_bytes))
577 } else {
578 None
579 }
580 }
581 }
582}
583
584#[derive(Default)]
588pub struct PackRangeAudit<'a> {
589 pub range_bytes: Option<&'a [u8]>,
592 pub authorized: Option<&'a BTreeSet<ManifestKey>>,
595}
596
597pub fn fsck_pack_range(claim: &PackRangeClaim, audit: &PackRangeAudit<'_>) -> FsckReport {
603 let mut report = FsckReport::default();
604
605 if claim.end < claim.start {
606 report.push(FsckFinding::new(
607 FsckRule::RangeCoverageMismatch,
608 None,
609 format!("range end {} precedes start {}", claim.end, claim.start),
610 ));
611 return report;
612 }
613
614 let records = claim.records();
615 if records.is_empty() {
616 if claim.end != claim.start {
617 report.push(FsckFinding::new(
618 FsckRule::RangeCoverageMismatch,
619 None,
620 format!(
621 "range covers {} bytes but claims no records",
622 claim.end - claim.start
623 ),
624 ));
625 }
626 return report;
627 }
628
629 let mut cursor = claim.start;
630 for (index, record) in records.iter().enumerate() {
631 if record.length == 0 {
632 report.push(FsckFinding::new(
633 FsckRule::ZeroLengthExtent,
634 None,
635 format!("record {index} ({}) claims zero bytes", record.object.hash),
636 ));
637 }
638
639 if index > 0 && record.offset < records[index - 1].offset {
640 report.push(FsckFinding::new(
641 FsckRule::ExtentsOutOfOffsetOrder,
642 None,
643 format!(
644 "record {index} at offset {} follows offset {}",
645 record.offset,
646 records[index - 1].offset
647 ),
648 ));
649 }
650
651 if record.offset > cursor {
652 report.push(FsckFinding::new(
653 FsckRule::ExtentGap,
654 None,
655 format!(
656 "unclaimed bytes [{cursor}, {}) before record {index}",
657 record.offset
658 ),
659 ));
660 } else if record.offset < cursor {
661 report.push(FsckFinding::new(
662 FsckRule::ExtentOverlap,
663 None,
664 format!(
665 "record {index} starts at {} inside claimed bytes ending at {cursor}",
666 record.offset
667 ),
668 ));
669 }
670
671 let Some(end) = record.end() else {
672 report.push(FsckFinding::new(
673 FsckRule::RangeCoverageMismatch,
674 None,
675 format!("record {index} offset + length overflows u64"),
676 ));
677 return report;
678 };
679 cursor = cursor.max(end);
680
681 if let (Some(bytes), Some(record_end)) = (audit.range_bytes, record.end())
682 && record.offset >= claim.start
683 {
684 let from = (record.offset - claim.start) as usize;
685 let to = record_end.saturating_sub(claim.start) as usize;
686 match bytes.get(from..to) {
687 Some(slice) => {
688 let digest = ContentHash::compute(slice);
689 if digest != record.encoded_digest {
690 report.push(FsckFinding::new(
691 FsckRule::ExtentDigestMismatch,
692 None,
693 format!(
694 "record {index} ({}) hashes to {digest}, not {}",
695 record.object.hash, record.encoded_digest
696 ),
697 ));
698 }
699 }
700 None => {
701 report.push(FsckFinding::new(
702 FsckRule::RangeCoverageMismatch,
703 None,
704 format!("record {index} extends past the supplied range bytes"),
705 ));
706 }
707 }
708 }
709
710 if let Some(authorized) = audit.authorized
711 && !authorized.contains(&record.key())
712 {
713 report.push(FsckFinding::new(
714 FsckRule::ExtentObjectNotInManifest,
715 None,
716 format!(
717 "record {index} authorizes {} object {}, which the manifest does not cover",
718 record.object.kind, record.object.hash
719 ),
720 ));
721 }
722 }
723
724 if cursor != claim.end {
725 report.push(FsckFinding::new(
726 FsckRule::RangeCoverageMismatch,
727 None,
728 format!(
729 "records cover through {cursor} but the range ends at {}",
730 claim.end
731 ),
732 ));
733 }
734
735 if let Some(bytes) = audit.range_bytes
736 && let Some(expected) = claim.byte_len()
737 && bytes.len() as u64 != expected
738 {
739 report.push(FsckFinding::new(
740 FsckRule::RangeCoverageMismatch,
741 None,
742 format!("supplied {} bytes for a {expected}-byte range", bytes.len()),
743 ));
744 }
745
746 report
747}