use std::collections::{BTreeMap, BTreeSet};
use super::{
build::{ManifestNodeSource, ManifestNodeStore, build_manifest},
extent::PackRangeClaim,
node::{
MANIFEST_LEAF_MAX_ENTRIES, MANIFEST_ROUTE_LEVELS, ManifestDecodeError, ManifestKey,
ManifestNode, ManifestObject,
},
};
use crate::object::ContentHash;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FsckRule {
MalformedNode,
NonCanonicalNodeEncoding,
TrailingBytes,
LeafEntriesOutOfOrder,
DuplicateObjectKey,
EmptyBranchBitmap,
NodeDigestMismatch,
SubtreeSummaryMismatch,
ObjectSizeMismatch,
ExtentDigestMismatch,
DanglingNodeRef,
DanglingObjectRef,
ExtentObjectNotInManifest,
UnreachableNode,
LeafOverfull,
UnderfullBranch,
EmptyNonRootLeaf,
BranchDepthMismatch,
MisroutedEntry,
NonCanonicalTrieShape,
DepthExceeded,
ExtentsOutOfOffsetOrder,
ExtentOverlap,
ExtentGap,
RangeCoverageMismatch,
ZeroLengthExtent,
}
impl FsckRule {
pub fn name(self) -> &'static str {
match self {
Self::MalformedNode => "malformed-node",
Self::NonCanonicalNodeEncoding => "non-canonical-node-encoding",
Self::TrailingBytes => "trailing-bytes",
Self::LeafEntriesOutOfOrder => "leaf-entries-out-of-order",
Self::DuplicateObjectKey => "duplicate-object-key",
Self::EmptyBranchBitmap => "empty-branch-bitmap",
Self::NodeDigestMismatch => "node-digest-mismatch",
Self::SubtreeSummaryMismatch => "subtree-summary-mismatch",
Self::ObjectSizeMismatch => "object-size-mismatch",
Self::ExtentDigestMismatch => "extent-digest-mismatch",
Self::DanglingNodeRef => "dangling-node-ref",
Self::DanglingObjectRef => "dangling-object-ref",
Self::ExtentObjectNotInManifest => "extent-object-not-in-manifest",
Self::UnreachableNode => "unreachable-node",
Self::LeafOverfull => "leaf-overfull",
Self::UnderfullBranch => "underfull-branch",
Self::EmptyNonRootLeaf => "empty-non-root-leaf",
Self::BranchDepthMismatch => "branch-depth-mismatch",
Self::MisroutedEntry => "misrouted-entry",
Self::NonCanonicalTrieShape => "non-canonical-trie-shape",
Self::DepthExceeded => "depth-exceeded",
Self::ExtentsOutOfOffsetOrder => "extents-out-of-offset-order",
Self::ExtentOverlap => "extent-overlap",
Self::ExtentGap => "extent-gap",
Self::RangeCoverageMismatch => "range-coverage-mismatch",
Self::ZeroLengthExtent => "zero-length-extent",
}
}
}
impl std::fmt::Display for FsckRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
impl ManifestDecodeError {
pub fn fsck_rule(&self) -> FsckRule {
match self {
Self::BadMagic
| Self::UnsupportedVersion(_)
| Self::UnknownNodeTag(_)
| Self::UnknownObjectKind(_)
| Self::Truncated => FsckRule::MalformedNode,
Self::TrailingBytes => FsckRule::TrailingBytes,
Self::EntriesOutOfOrder => FsckRule::LeafEntriesOutOfOrder,
Self::DuplicateObjectKey(_) => FsckRule::DuplicateObjectKey,
Self::EmptyBranchBitmap => FsckRule::EmptyBranchBitmap,
Self::NonCanonicalEncoding => FsckRule::NonCanonicalNodeEncoding,
Self::AddressMismatch { .. } => FsckRule::NodeDigestMismatch,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsckFinding {
pub rule: FsckRule,
pub node: Option<ContentHash>,
pub detail: String,
}
impl FsckFinding {
fn new(rule: FsckRule, node: Option<ContentHash>, detail: impl Into<String>) -> Self {
Self {
rule,
node,
detail: detail.into(),
}
}
}
impl std::fmt::Display for FsckFinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.node {
Some(node) => write!(f, "{}: {} ({})", self.rule, self.detail, node.short()),
None => write!(f, "{}: {}", self.rule, self.detail),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FsckReport {
findings: Vec<FsckFinding>,
}
impl FsckReport {
pub fn findings(&self) -> &[FsckFinding] {
&self.findings
}
pub fn is_clean(&self) -> bool {
self.findings.is_empty()
}
pub fn violated_rules(&self) -> BTreeSet<FsckRule> {
self.findings.iter().map(|finding| finding.rule).collect()
}
pub fn has_rule(&self, rule: FsckRule) -> bool {
self.findings.iter().any(|finding| finding.rule == rule)
}
fn push(&mut self, finding: FsckFinding) {
self.findings.push(finding);
}
}
pub trait ManifestObjectIndex {
fn decoded_size(&self, key: &ManifestKey) -> Option<u64>;
}
impl ManifestObjectIndex for BTreeMap<ManifestKey, u64> {
fn decoded_size(&self, key: &ManifestKey) -> Option<u64> {
self.get(key).copied()
}
}
impl ManifestObjectIndex for std::collections::HashMap<ManifestKey, u64> {
fn decoded_size(&self, key: &ManifestKey) -> Option<u64> {
self.get(key).copied()
}
}
#[derive(Default)]
pub struct FsckOptions<'a> {
pub objects: Option<&'a dyn ManifestObjectIndex>,
pub report_unreachable: bool,
}
pub fn fsck_manifest<S: ManifestNodeSource + ?Sized>(source: &S, root: &ContentHash) -> FsckReport {
fsck_manifest_with(source, root, &FsckOptions::default())
}
pub fn fsck_manifest_with<S: ManifestNodeSource + ?Sized>(
source: &S,
root: &ContentHash,
options: &FsckOptions<'_>,
) -> FsckReport {
let mut report = FsckReport::default();
let mut visited = BTreeSet::new();
let mut objects = Vec::new();
let mut expansion_complete = true;
visit(
source,
root,
0,
true,
options,
&mut visited,
&mut objects,
&mut report,
&mut expansion_complete,
);
if expansion_complete
&& let Ok(rebuilt) = build_manifest(objects.iter().copied())
&& rebuilt.root != *root
{
report.push(FsckFinding::new(
FsckRule::NonCanonicalTrieShape,
Some(*root),
format!(
"rebuilding from {} expanded objects yields root {}",
objects.len(),
rebuilt.root
),
));
}
report
}
pub fn fsck_manifest_store<S: ManifestNodeStore + ?Sized>(
store: &S,
roots: &[ContentHash],
options: &FsckOptions<'_>,
) -> FsckReport {
let mut report = FsckReport::default();
let mut reachable = BTreeSet::new();
for root in roots {
let mut visited = BTreeSet::new();
let mut objects = Vec::new();
let mut expansion_complete = true;
visit(
store,
root,
0,
true,
options,
&mut visited,
&mut objects,
&mut report,
&mut expansion_complete,
);
if expansion_complete
&& let Ok(rebuilt) = build_manifest(objects.iter().copied())
&& rebuilt.root != *root
{
report.push(FsckFinding::new(
FsckRule::NonCanonicalTrieShape,
Some(*root),
format!("rebuilt root {} differs", rebuilt.root),
));
}
reachable.extend(visited);
}
if options.report_unreachable {
for hash in store.node_hashes() {
if !reachable.contains(&hash) {
report.push(FsckFinding::new(
FsckRule::UnreachableNode,
Some(hash),
"node is present but no supplied root reaches it",
));
}
}
}
report
}
#[allow(clippy::too_many_arguments)]
fn visit<S: ManifestNodeSource + ?Sized>(
source: &S,
hash: &ContentHash,
depth: u8,
is_root: bool,
options: &FsckOptions<'_>,
visited: &mut BTreeSet<ContentHash>,
objects: &mut Vec<ManifestObject>,
report: &mut FsckReport,
expansion_complete: &mut bool,
) -> Option<(u64, u64)> {
if depth > MANIFEST_ROUTE_LEVELS {
*expansion_complete = false;
report.push(FsckFinding::new(
FsckRule::DepthExceeded,
Some(*hash),
format!("traversal passed the fixed {MANIFEST_ROUTE_LEVELS}-level route"),
));
return None;
}
if !visited.insert(*hash) {
return None;
}
let Some(bytes) = source.node_bytes(hash) else {
*expansion_complete = false;
report.push(FsckFinding::new(
FsckRule::DanglingNodeRef,
Some(*hash),
"node is referenced but absent from the node source",
));
return None;
};
let actual = ContentHash::compute(bytes);
if actual != *hash {
*expansion_complete = false;
report.push(FsckFinding::new(
FsckRule::NodeDigestMismatch,
Some(*hash),
format!("bytes hash to {actual}"),
));
return None;
}
let node = match ManifestNode::decode(bytes) {
Ok(node) => node,
Err(error) => {
*expansion_complete = false;
report.push(FsckFinding::new(
error.fsck_rule(),
Some(*hash),
error.to_string(),
));
return None;
}
};
match node {
ManifestNode::Leaf(leaf) => {
let entries = leaf.entries();
if entries.is_empty() && !is_root {
report.push(FsckFinding::new(
FsckRule::EmptyNonRootLeaf,
Some(*hash),
"only the root may be the empty leaf",
));
}
if entries.len() > MANIFEST_LEAF_MAX_ENTRIES && depth < MANIFEST_ROUTE_LEVELS {
report.push(FsckFinding::new(
FsckRule::LeafOverfull,
Some(*hash),
format!(
"leaf at depth {depth} holds {} entries; the bound is {MANIFEST_LEAF_MAX_ENTRIES} while routing bits remain",
entries.len()
),
));
}
let mut decoded_bytes = 0u64;
for entry in entries {
decoded_bytes = decoded_bytes.saturating_add(entry.decoded_size);
if let Some(index) = options.objects {
match index.decoded_size(&entry.key()) {
None => {
report.push(FsckFinding::new(
FsckRule::DanglingObjectRef,
Some(*hash),
format!("{} object {} is not present", entry.kind, entry.hash),
));
}
Some(size) if size != entry.decoded_size => {
report.push(FsckFinding::new(
FsckRule::ObjectSizeMismatch,
Some(*hash),
format!(
"{} object {} declares {} bytes but holds {size}",
entry.kind, entry.hash, entry.decoded_size
),
));
}
Some(_) => {}
}
}
objects.push(*entry);
}
Some((entries.len() as u64, decoded_bytes))
}
ManifestNode::Branch(branch) => {
if branch.depth() != depth {
report.push(FsckFinding::new(
FsckRule::BranchDepthMismatch,
Some(*hash),
format!("declares depth {} but sits at {depth}", branch.depth()),
));
}
let mut total_count = 0u64;
let mut total_bytes = 0u64;
let mut all_children_summarized = true;
for child in branch.children() {
let before = objects.len();
let summary = visit(
source,
&child.hash,
depth + 1,
false,
options,
visited,
objects,
report,
expansion_complete,
);
for entry in &objects[before..] {
if entry.key().route().group(depth) != child.slot {
report.push(FsckFinding::new(
FsckRule::MisroutedEntry,
Some(child.hash),
format!(
"{} object {} routes to slot {} at depth {depth}, not {}",
entry.kind,
entry.hash,
entry.key().route().group(depth),
child.slot
),
));
}
}
match summary {
Some((count, bytes)) => {
if count != child.object_count || bytes != child.decoded_bytes {
report.push(FsckFinding::new(
FsckRule::SubtreeSummaryMismatch,
Some(*hash),
format!(
"slot {} summarizes ({}, {}) but holds ({count}, {bytes})",
child.slot, child.object_count, child.decoded_bytes
),
));
}
total_count += count;
total_bytes = total_bytes.saturating_add(bytes);
}
None => {
all_children_summarized = false;
}
}
}
if all_children_summarized {
if total_count <= MANIFEST_LEAF_MAX_ENTRIES as u64 && depth < MANIFEST_ROUTE_LEVELS
{
report.push(FsckFinding::new(
FsckRule::UnderfullBranch,
Some(*hash),
format!(
"branch subtree holds {total_count} objects; it must be a single leaf"
),
));
}
Some((total_count, total_bytes))
} else {
None
}
}
}
}
#[derive(Default)]
pub struct PackRangeAudit<'a> {
pub range_bytes: Option<&'a [u8]>,
pub authorized: Option<&'a BTreeSet<ManifestKey>>,
}
pub fn fsck_pack_range(claim: &PackRangeClaim, audit: &PackRangeAudit<'_>) -> FsckReport {
let mut report = FsckReport::default();
if claim.end < claim.start {
report.push(FsckFinding::new(
FsckRule::RangeCoverageMismatch,
None,
format!("range end {} precedes start {}", claim.end, claim.start),
));
return report;
}
let records = claim.records();
if records.is_empty() {
if claim.end != claim.start {
report.push(FsckFinding::new(
FsckRule::RangeCoverageMismatch,
None,
format!(
"range covers {} bytes but claims no records",
claim.end - claim.start
),
));
}
return report;
}
let mut cursor = claim.start;
for (index, record) in records.iter().enumerate() {
if record.length == 0 {
report.push(FsckFinding::new(
FsckRule::ZeroLengthExtent,
None,
format!("record {index} ({}) claims zero bytes", record.object.hash),
));
}
if index > 0 && record.offset < records[index - 1].offset {
report.push(FsckFinding::new(
FsckRule::ExtentsOutOfOffsetOrder,
None,
format!(
"record {index} at offset {} follows offset {}",
record.offset,
records[index - 1].offset
),
));
}
if record.offset > cursor {
report.push(FsckFinding::new(
FsckRule::ExtentGap,
None,
format!(
"unclaimed bytes [{cursor}, {}) before record {index}",
record.offset
),
));
} else if record.offset < cursor {
report.push(FsckFinding::new(
FsckRule::ExtentOverlap,
None,
format!(
"record {index} starts at {} inside claimed bytes ending at {cursor}",
record.offset
),
));
}
let Some(end) = record.end() else {
report.push(FsckFinding::new(
FsckRule::RangeCoverageMismatch,
None,
format!("record {index} offset + length overflows u64"),
));
return report;
};
cursor = cursor.max(end);
if let (Some(bytes), Some(record_end)) = (audit.range_bytes, record.end())
&& record.offset >= claim.start
{
let from = (record.offset - claim.start) as usize;
let to = record_end.saturating_sub(claim.start) as usize;
match bytes.get(from..to) {
Some(slice) => {
let digest = ContentHash::compute(slice);
if digest != record.encoded_digest {
report.push(FsckFinding::new(
FsckRule::ExtentDigestMismatch,
None,
format!(
"record {index} ({}) hashes to {digest}, not {}",
record.object.hash, record.encoded_digest
),
));
}
}
None => {
report.push(FsckFinding::new(
FsckRule::RangeCoverageMismatch,
None,
format!("record {index} extends past the supplied range bytes"),
));
}
}
}
if let Some(authorized) = audit.authorized
&& !authorized.contains(&record.key())
{
report.push(FsckFinding::new(
FsckRule::ExtentObjectNotInManifest,
None,
format!(
"record {index} authorizes {} object {}, which the manifest does not cover",
record.object.kind, record.object.hash
),
));
}
}
if cursor != claim.end {
report.push(FsckFinding::new(
FsckRule::RangeCoverageMismatch,
None,
format!(
"records cover through {cursor} but the range ends at {}",
claim.end
),
));
}
if let Some(bytes) = audit.range_bytes
&& let Some(expected) = claim.byte_len()
&& bytes.len() as u64 != expected
{
report.push(FsckFinding::new(
FsckRule::RangeCoverageMismatch,
None,
format!("supplied {} bytes for a {expected}-byte range", bytes.len()),
));
}
report
}