OXYGENHUB

Coverage / crates/oxy-core/src/repo/fsck.rs 71.6%

265 of 370 lines run, 34 of 64 functions · run #420 on main · run missed

1
//! Repository integrity check (spec §21.1): `oxy fsck`.
2
//!
3
//! Reads everything and changes nothing. Four passes:
4
//!
5
//! 1. every stored object: header, length, hash, and that its body decodes
6
//!    as its type says;
7
//! 2. the operation log: every operation reachable from the head is
8
//!    present and decodes, and clocks never run backwards;
9
//! 3. references: the current view's bookmarks, tags, remote bookmarks,
10
//!    workspaces and policy, then the dependency closure of everything
11
//!    they name, with each change's author identity, blobs and chunks,
12
//!    and each change's signatures or seals;
13
//! 4. derived data: every bookmark's state validates, every checkpoint
14
//!    restores exactly the state it names, and reference and tag
15
//!    attestations carry good signatures.
16
//!
17
//! Missing blobs are warnings, not errors, in a partial clone
18
//! (`filter.max-blob` or a sparse filter), and changes beyond a shallow
19
//! boundary are expected to be absent.
20
21
use super::*;
22
use std::collections::{HashSet, VecDeque};
23
24
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
25
pub enum Severity {
26
    /// Worth knowing; the repository is still sound.
27
    Warning,
28
    /// Data is missing, corrupt or fails verification.
29
    Error,
30
}
31
32
#[derive(Clone, Debug, PartialEq, Eq)]
33
pub struct Problem {
34
    pub severity: Severity,
35
    /// The object the problem is about, when there is one.
36
    pub object: Option<ObjectRef>,
37
    pub what: String,
38
}
39
40
#[derive(Clone, Debug, Default, PartialEq, Eq)]
41
pub struct FsckReport {
42
    pub objects: usize,
43
    pub ops: usize,
44
    /// Changes checked, working-copy changes included.
45
    pub changes: usize,
46
    /// Committed changes authenticated by their own signature or by a seal.
47
    pub signed: usize,
48
    pub unsigned: usize,
49
    pub checkpoints: usize,
50
    pub attestations: usize,
51
    pub problems: Vec<Problem>,
52
}
53
54
impl FsckReport {
5568×
    pub fn errors(&self) -> usize {
5668×
        self.problems
5768×
            .iter()
5868×
            .filter(|p| p.severity == Severity::Error)
5968×
            .count()
6068×
    }
61
620×
    pub fn warnings(&self) -> usize {
630×
        self.problems
640×
            .iter()
650×
            .filter(|p| p.severity == Severity::Warning)
660×
            .count()
670×
    }
68
6911×
    fn error(&mut self, object: Option<ObjectRef>, what: impl Into<String>) {
7011×
        self.problems.push(Problem {
7111×
            severity: Severity::Error,
7211×
            object,
7311×
            what: what.into(),
7411×
        });
7511×
    }
76
770×
    fn warn(&mut self, object: Option<ObjectRef>, what: impl Into<String>) {
780×
        self.problems.push(Problem {
790×
            severity: Severity::Warning,
800×
            object,
810×
            what: what.into(),
820×
        });
830×
    }
84
}
85
86
/// Whether `body` decodes as an object of type `ty`.
871730×
fn decodes(ty: ObjectType, body: &[u8]) -> std::result::Result<(), String> {
881730×
    let r = match ty {
89137×
        ObjectType::Chunk => Ok(()),
9086×
        ObjectType::Blob => Blob::decode(body).map(|_| ()),
91
        // Structure only: the text blobs are checked with the change graph.
92
        ObjectType::Change => {
93432×
            Change::decode_with(body, &mut |_, _, _| Ok(crate::lines::Lines::default())).map(|_| ())
94
        }
9512×
        ObjectType::Checkpoint => crate::objects::Checkpoint::decode(body).map(|_| ()),
96492×
        ObjectType::Op => match Op::decode(body) {
97492×
            Ok(_) => Ok(()),
980×
            Err(e) => Op::decode_format1(body).map(|_| ()).map_err(|_| e),
99
        },
100230×
        ObjectType::Attest => Attest::decode(body).map(|_| ()),
10170×
        ObjectType::Identity => Identity::decode(body).map(|_| ()),
1020×
        ObjectType::Policy => crate::policy::Policy::decode(body).map(|_| ()),
1032×
        ObjectType::Receipt => crate::receipt::Receipt::decode(body).map(|_| ()),
1041×
        ObjectType::Certificate => crate::cert::Certificate::decode(body).map(|_| ()),
1050×
        ObjectType::Grant => crate::cert::Grant::decode(body).map(|_| ()),
1061×
        ObjectType::Artifact => crate::artifact::Artifact::decode(body).map(|_| ()),
107267×
        ObjectType::State => crate::objects::StateObject::decode(body).map(|_| ()),
108
    };
1091730×
    r.map_err(|e| e.to_string())
1101730×
}
111
112
impl Repo {
113
    /// Check the whole repository; see the module documentation.
11480×
    pub fn fsck(&self) -> Result<FsckReport> {
115
        // The head as it is now, as the operations pass reads it.
11680×
        self.forget_head();
11780×
        let mut rep = FsckReport::default();
11880×
        let bad = {
11980×
            let _t = crate::timing::phase("fsck: objects");
12080×
            self.fsck_objects(&mut rep)?
121
        };
12280×
        {
12380×
            let _t = crate::timing::phase("fsck: operations");
12480×
            self.fsck_ops(&mut rep, &bad);
12580×
        }
126
        {
12780×
            let _t = crate::timing::phase("fsck: references");
12880×
            self.fsck_references(&mut rep, &bad)?;
129
        }
130
        {
13180×
            let _t = crate::timing::phase("fsck: states and checkpoints");
13280×
            self.fsck_derived(&mut rep)?;
133
        }
13480×
        rep.problems.sort_by_key(|p| std::cmp::Reverse(p.severity));
13580×
        Ok(rep)
13680×
    }
137
138
    /// Pass 1. Returns the objects that failed, so later passes report
139
    /// them once rather than at every reference.
14080×
    fn fsck_objects(&self, rep: &mut FsckReport) -> Result<HashSet<ObjectRef>> {
141
        use rayon::prelude::*;
14280×
        let mut bad = HashSet::new();
143
        // A chunk at a time, read (and verified) and decoded on every core.
14480×
        for refs in self.store.list()?.chunks(8192) {
14580×
            let checked: Vec<std::result::Result<(), String>> = self
14680×
                .store
14780×
                .get_many(refs)
14880×
                .into_par_iter()
1491731×
                .map(|got| match got {
1501730×
                    Ok((ty, body)) => decodes(ty, &body)
1511730×
                        .map_err(|e| format!("{} object does not decode: {e}", ty.name())),
1521×
                    Err(e) => Err(format!("unreadable object: {e}")),
1531731×
                })
15480×
                .collect();
1551731×
            for (r, c) in refs.iter().zip(checked) {
1561731×
                rep.objects += 1;
1571731×
                if let Err(e) = c {
1581×
                    rep.error(Some(*r), e);
1591×
                    bad.insert(*r);
1601730×
                }
161
            }
162
        }
16380×
        Ok(bad)
16480×
    }
165
166
    /// Pass 2.
16780×
    fn fsck_ops(&self, rep: &mut FsckReport, bad: &HashSet<ObjectRef>) {
16880×
        let head = match self.read_head() {
16979×
            Ok((h, _)) => h,
1701×
            Err(RepoError::Msg(m)) if m.starts_with("corrupt") => {
1711×
                rep.error(None, ".oxy/ops/head does not hold an object reference");
1721×
                return;
173
            }
1740×
            Err(e) => {
1750×
                rep.error(None, format!("cannot read .oxy/ops/head: {e}"));
1760×
                return;
177
            }
178
        };
17979×
        let mut seen = HashSet::new();
18079×
        let mut queue: VecDeque<(ObjectRef, Option<(ObjectRef, u64)>)> =
18179×
            VecDeque::from([(head, None)]);
182573×
        while let Some((r, child)) = queue.pop_front() {
183494×
            if !seen.insert(r) {
18420×
                continue;
185474×
            }
186474×
            if bad.contains(&r) {
1870×
                continue;
188474×
            }
189474×
            let body = match self.store.get_typed(&r, ObjectType::Op) {
190474×
                Ok(b) => b,
191
                Err(StoreError::NotFound(_)) => {
1920×
                    let by = child
1930×
                        .map(|(c, _)| format!(", the parent of operation {}", c.short(12)))
1940×
                        .unwrap_or_default();
1950×
                    rep.error(
1960×
                        Some(r),
1970×
                        format!("operation missing{by}; the log is cut here"),
198
                    );
1990×
                    continue;
200
                }
2010×
                Err(e) => {
2020×
                    rep.error(Some(r), format!("operation unreadable: {e}"));
2030×
                    continue;
204
                }
205
            };
206474×
            rep.ops += 1;
207474×
            let (parents, wall_ms) = match Op::decode(&body) {
208474×
                Ok(op) => (op.parents, op.hlc.wall_ms),
2090×
                Err(_) => match Op::decode_format1(&body) {
2100×
                    Ok(op) => (op.parents, op.hlc.wall_ms),
2110×
                    Err(e) => {
2120×
                        rep.error(Some(r), format!("operation does not decode: {e}"));
2130×
                        continue;
214
                    }
215
                },
216
            };
217474×
            if let Some((c, child_ms)) = child {
218395×
                if wall_ms > child_ms {
2190×
                    rep.warn(
2200×
                        Some(c),
2210×
                        format!(
2220×
                            "operation is timed before its parent {} (a clock was wrong)",
2230×
                            r.short(12)
2240×
                        ),
2250×
                    );
226395×
                }
22779×
            }
228474×
            queue.extend(parents.into_iter().map(|p| (p, Some((r, wall_ms)))));
229
        }
23080×
    }
231
232
    /// Pass 3.
23380×
    fn fsck_references(&self, rep: &mut FsckReport, bad: &HashSet<ObjectRef>) -> Result<()> {
23480×
        let view = match self.view() {
23579×
            Ok(v) => v,
2361×
            Err(e) => {
2371×
                rep.error(None, format!("cannot read the current view: {e}"));
2381×
                return Ok(());
239
            }
240
        };
24179×
        let boundary = self.boundary();
24279×
        let partial = self.config.max_blob.is_some() || !self.config.sparse_paths.is_empty();
243
24479×
        let mut roots: Vec<(ChangeHash, String)> = Vec::new();
24579×
        let mut states: Vec<(ObjectRef, String)> = Vec::new();
24679×
        states.extend(
24779×
            view.bookmarks
24879×
                .iter()
24980×
                .map(|(n, s)| (*s, format!("bookmark {n}"))),
250
        );
25179×
        states.extend(view.tags.iter().map(|(n, s)| (*s, format!("tag {n}"))));
25279×
        for (remote, bms) in &view.remotes {
25348×
            states.extend(bms.iter().map(|(n, s)| (*s, format!("{remote}/{n}"))));
254
        }
255136×
        for (st, what) in states {
256136×
            match self.store.get_state(&st) {
257196×
                Ok(s) => roots.extend(s.heads.into_iter().map(|h| (h, what.clone()))),
2580×
                Err(e) if !bad.contains(&st) => {
2590×
                    rep.error(Some(st), format!("state of {what} unusable: {e}"))
260
                }
2610×
                Err(_) => {}
262
            }
263
        }
26479×
        for (ws, h) in &view.workspaces {
26579×
            roots.push((*h, format!("workspace {}", ws.to_hex())));
26679×
        }
26779×
        if let Some(p) = view.policy {
2680×
            match self.store.get_typed(&p, ObjectType::Policy) {
2690×
                Ok(bytes) => {
2700×
                    if let Ok(policy) = crate::policy::Policy::decode(&bytes) {
2710×
                        for id in &policy.identities {
2720×
                            if !self.store.has(&id.hash()) {
2730×
                                rep.warn(
2740×
                                    Some(id.hash()),
2750×
                                    "an identity the policy names is not stored",
2760×
                                );
2770×
                            }
278
                        }
2790×
                    }
280
                }
2810×
                Err(e) if !bad.contains(&p) => {
2820×
                    rep.error(Some(p), format!("the policy is unusable: {e}"))
283
                }
2840×
                Err(_) => {}
285
            }
28679×
        }
287
288
        // Working-copy changes are never signed; they are checked but not tallied.
28979×
        let working: HashSet<ChangeHash> = view.workspaces.values().copied().collect();
29079×
        let mut seen: HashSet<ChangeHash> = HashSet::new();
29179×
        let mut blobs_seen: HashSet<ObjectRef> = HashSet::new();
29279×
        let mut stack = roots;
293494×
        while let Some((h, needed_by)) = stack.pop() {
294415×
            if !seen.insert(h) {
295175×
                continue;
296240×
            }
297
            // The skeleton: the text is checked as the blobs it is stored
298
            // in, not decoded (the objects pass checked every object).
299240×
            let (c, texts) = match self.store.get_change_lazy(&h) {
300238×
                Ok(c) => c,
3011×
                Err(StoreError::NotFound(_)) if boundary.contains(&h) => continue,
302
                Err(StoreError::NotFound(_)) => {
3031×
                    rep.error(Some(h), format!("change missing, needed by {needed_by}"));
3041×
                    continue;
305
                }
3061×
                Err(e) => {
3071×
                    if !bad.contains(&h) {
3080×
                        rep.error(Some(h), format!("change unusable: {e}"));
3091×
                    }
3101×
                    continue;
311
                }
312
            };
313238×
            rep.changes += 1;
314238×
            let label = format!("change {}", h.short(12));
315238×
            stack.extend(c.deps.iter().map(|d| (*d, label.clone())));
316238×
            if !c.author.is_zero() && !self.store.has(&c.author) {
3170×
                rep.warn(
3180×
                    Some(h),
3190×
                    format!("author identity {} is not stored", c.author.short(12)),
3200×
                );
321238×
            }
322238×
            for t in texts {
323198×
                if let crate::objects::TextRef::Blob(b, _, _) = t {
32446×
                    if blobs_seen.insert(b) {
32546×
                        // A change cannot be read without its text: never partial.
32646×
                        self.fsck_blob(rep, b, &label, false, bad);
32746×
                    }
328152×
                }
329
            }
330238×
            for e in &c.edits {
331213×
                let Edit::Replace { old, new, .. } = e else {
332201×
                    continue;
333
                };
33414×
                for b in [old, new].into_iter().flatten() {
33514×
                    if !blobs_seen.insert(*b) {
3362×
                        continue;
33712×
                    }
33812×
                    self.fsck_blob(rep, *b, &label, partial, bad);
339
                }
340
            }
341238×
            if working.contains(&h) {
34279×
                continue;
343159×
            }
344159×
            match self.authenticate_change(&h) {
345105×
                Ok(SignatureStatus::Valid { .. }) => rep.signed += 1,
34654×
                Ok(SignatureStatus::Unsigned) => rep.unsigned += 1,
3470×
                Ok(SignatureStatus::Invalid(why)) => {
3480×
                    rep.error(Some(h), format!("bad signature: {why}"))
349
                }
3500×
                Err(e) => rep.error(Some(h), format!("cannot check signatures: {e}")),
351
            }
352
        }
35379×
        Ok(())
35480×
    }
355
35658×
    fn fsck_blob(
35758×
        &self,
35858×
        rep: &mut FsckReport,
35958×
        b: ObjectRef,
36058×
        used_by: &str,
36158×
        partial: bool,
36258×
        bad: &HashSet<ObjectRef>,
36358×
    ) {
36458×
        match self.store.get_typed(&b, ObjectType::Blob) {
36556×
            Ok(bytes) => {
36656×
                let Ok(blob) = Blob::decode(&bytes) else {
3670×
                    return;
368
                };
36995×
                for ch in blob.chunks {
37095×
                    if !self.store.has(&ch) {
3710×
                        rep.error(
3720×
                            Some(ch),
3730×
                            format!("chunk missing from blob {} ({used_by})", b.short(12)),
3740×
                        );
37595×
                    }
376
                }
377
            }
3780×
            Err(StoreError::NotFound(_)) if partial => rep.warn(
3790×
                Some(b),
3800×
                format!("blob not fetched ({used_by}); expected in a partial clone"),
381
            ),
3822×
            Err(StoreError::NotFound(_)) => rep.error(Some(b), format!("blob missing ({used_by})")),
3830×
            Err(e) if !bad.contains(&b) => {
3840×
                rep.error(Some(b), format!("blob unusable ({used_by}): {e}"))
385
            }
3860×
            Err(_) => {}
387
        }
38858×
    }
389
390
    /// Whether `heads` form a valid state: its history loads and every file
391
    /// graph builds. Graphs are built a batch at a time and dropped, so the
392
    /// check needs the memory of a batch, not of the whole tree.
39380×
    fn check_state(&self, heads: &[ChangeHash]) -> Result<()> {
39480×
        let mut st = self.load_state_history(heads)?;
39580×
        let nodes = st.unbuilt_nodes();
39680×
        for batch in nodes.chunks(2048) {
39776×
            self.load_graphs_of(&mut st, batch)?;
39874×
            st.drop_graphs(batch);
399
        }
40078×
        Ok(())
40180×
    }
402
403
    /// Pass 4.
40480×
    fn fsck_derived(&self, rep: &mut FsckReport) -> Result<()> {
40580×
        let Ok(view) = self.view() else { return Ok(()) };
40679×
        let shallow = self.is_shallow();
40779×
        if !shallow {
40879×
            let mut checked: HashSet<ObjectRef> = HashSet::new();
40980×
            for (name, st) in &view.bookmarks {
41080×
                if !checked.insert(*st) {
4110×
                    continue;
41280×
                }
41380×
                let Ok(s) = self.store.get_state(st) else {
4140×
                    continue;
415
                };
41680×
                if let Err(e) = self.check_state(&s.heads) {
4172×
                    rep.error(
4182×
                        Some(*st),
4192×
                        format!("bookmark {name} does not form a valid state: {e}"),
4202×
                    );
42178×
                }
422
            }
4230×
        }
42479×
        for r in self.checkpoint_refs()? {
4259×
            rep.checkpoints += 1;
4269×
            let cp = match self.store.get_typed(&r, ObjectType::Checkpoint) {
4279×
                Ok(b) => match crate::objects::Checkpoint::decode(&b) {
4289×
                    Ok(cp) => cp,
4290×
                    Err(_) => continue,
430
                },
431
                Err(StoreError::NotFound(_)) => {
4320×
                    rep.warn(
4330×
                        Some(r),
434
                        "checkpoint pointer names a missing checkpoint; it will be rewritten",
435
                    );
4360×
                    continue;
437
                }
4380×
                Err(_) => continue,
439
            };
4409×
            let payload = match self.read_blob(&cp.payload) {
4419×
                Ok(p) => p,
4420×
                Err(e) => {
4430×
                    rep.error(Some(r), format!("checkpoint payload unreadable: {e}"));
4440×
                    continue;
445
                }
446
            };
4479×
            if let Err(e) = self.state_from_checkpoint(&payload) {
4481×
                rep.error(Some(r), format!("checkpoint payload does not decode: {e}"));
4491×
                continue;
4508×
            }
451
            // The payload must be what the named changes produce, in its own version.
4528×
            let version = crate::cbor::decode(&payload).ok().and_then(|v| {
4538×
                crate::objects::map(&v, "payload").ok().and_then(|m| {
4548×
                    m.get(&0)
4558×
                        .and_then(|x| crate::objects::uint(x, "version").ok())
4568×
                })
4578×
            });
4588×
            let expected = |full: &State| {
4596×
                if version == Some(crate::state::CHECKPOINT_INDEX_VERSION) {
4605×
                    full.checkpoint_index(&full.checkpoint_file_graphs(), Repo::blob_ref)
461
                } else {
4621×
                    full.checkpoint_payload()
463
                }
4646×
            };
4658×
            if !shallow {
4668×
                match self.load_state_full(&cp.heads) {
4676×
                    Ok(full) if expected(&full) != payload => {
4680×
                        rep.error(Some(r), "checkpoint disagrees with the changes it names")
469
                    }
4706×
                    Ok(_) => {}
4712×
                    Err(e) => rep.error(
4722×
                        Some(r),
4732×
                        format!("checkpoint names changes that do not form a state: {e}"),
474
                    ),
475
                }
4760×
            }
477
        }
47879×
        let mut attests = Vec::new();
47980×
        for name in view.bookmarks.keys() {
48080×
            if let Ok(Some((r, _))) = self.latest_ref_attest(name) {
48121×
                attests.push((r, format!("bookmark {name}")));
48259×
            }
483
        }
48479×
        for name in view.tags.keys() {
4858×
            if let Ok(Some((r, _))) = self.tag_attest(name) {
4868×
                attests.push((r, format!("tag {name}")));
4878×
            }
488
        }
48979×
        for (r, what) in attests {
49029×
            rep.attestations += 1;
49129×
            let sigs = self.store.signatures(&r)?;
49229×
            if sigs.is_empty() {
4930×
                rep.warn(Some(r), format!("attestation for {what} is unsigned"));
49429×
            }
49529×
            for sig in sigs {
49629×
                match self.store.get_identity(&sig.key) {
49729×
                    Ok(id) => {
49829×
                        if let Err(e) = verify_signature(&id, &sig, Domain::Attest, &r) {
4990×
                            rep.error(
5000×
                                Some(r),
5010×
                                format!("attestation for {what} has a bad signature: {e}"),
5020×
                            );
50329×
                        }
504
                    }
5050×
                    Err(_) => rep.warn(
5060×
                        Some(r),
5070×
                        format!(
508
                            "attestation for {what} is signed by {}, whose identity is not stored",
5090×
                            sig.key.short(12)
510
                        ),
511
                    ),
512
                }
513
            }
514
        }
51579×
        Ok(())
51680×
    }
517
}