//! Repository integrity check (spec ยง21.1): `oxy fsck`. //! //! Reads everything and changes nothing. Four passes: //! //! 1. every stored object: header, length, hash, and that its body decodes //! as its type says; //! 2. the operation log: every operation reachable from the head is //! present and decodes, and clocks never run backwards; //! 3. references: the current view's bookmarks, tags, remote bookmarks, //! workspaces and policy, then the dependency closure of everything //! they name, with each change's author identity, blobs and chunks, //! and each change's signatures or seals; //! 4. derived data: every bookmark's state validates, every checkpoint //! restores exactly the state it names, and reference and tag //! attestations carry good signatures. //! //! Missing blobs are warnings, not errors, in a partial clone //! (`filter.max-blob` or a sparse filter), and changes beyond a shallow //! boundary are expected to be absent. use super::*; use std::collections::{HashSet, VecDeque}; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Severity { /// Worth knowing; the repository is still sound. Warning, /// Data is missing, corrupt or fails verification. Error, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Problem { pub severity: Severity, /// The object the problem is about, when there is one. pub object: Option, pub what: String, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FsckReport { pub objects: usize, pub ops: usize, /// Changes checked, working-copy changes included. pub changes: usize, /// Committed changes authenticated by their own signature or by a seal. pub signed: usize, pub unsigned: usize, pub checkpoints: usize, pub attestations: usize, pub problems: Vec, } impl FsckReport { pub fn errors(&self) -> usize { self.problems .iter() .filter(|p| p.severity == Severity::Error) .count() } pub fn warnings(&self) -> usize { self.problems .iter() .filter(|p| p.severity == Severity::Warning) .count() } fn error(&mut self, object: Option, what: impl Into) { self.problems.push(Problem { severity: Severity::Error, object, what: what.into(), }); } fn warn(&mut self, object: Option, what: impl Into) { self.problems.push(Problem { severity: Severity::Warning, object, what: what.into(), }); } } /// Whether `body` decodes as an object of type `ty`. fn decodes(ty: ObjectType, body: &[u8]) -> std::result::Result<(), String> { let r = match ty { ObjectType::Chunk => Ok(()), ObjectType::Blob => Blob::decode(body).map(|_| ()), // Structure only: the text blobs are checked with the change graph. ObjectType::Change => { Change::decode_with(body, &mut |_, _| Ok(vec![Vec::new()])).map(|_| ()) } ObjectType::Checkpoint => crate::objects::Checkpoint::decode(body).map(|_| ()), ObjectType::Op => match Op::decode(body) { Ok(_) => Ok(()), Err(e) => Op::decode_format1(body).map(|_| ()).map_err(|_| e), }, ObjectType::Attest => Attest::decode(body).map(|_| ()), ObjectType::Identity => Identity::decode(body).map(|_| ()), ObjectType::Policy => crate::policy::Policy::decode(body).map(|_| ()), ObjectType::Receipt => crate::receipt::Receipt::decode(body).map(|_| ()), ObjectType::State => crate::objects::StateObject::decode(body).map(|_| ()), }; r.map_err(|e| e.to_string()) } impl Repo { /// Check the whole repository; see the module documentation. pub fn fsck(&self) -> Result { let mut rep = FsckReport::default(); let bad = { let _t = crate::timing::phase("fsck: objects"); self.fsck_objects(&mut rep)? }; { let _t = crate::timing::phase("fsck: operations"); self.fsck_ops(&mut rep, &bad); } { let _t = crate::timing::phase("fsck: references"); self.fsck_references(&mut rep, &bad)?; } { let _t = crate::timing::phase("fsck: states and checkpoints"); self.fsck_derived(&mut rep)?; } rep.problems.sort_by_key(|p| std::cmp::Reverse(p.severity)); Ok(rep) } /// Pass 1. Returns the objects that failed, so later passes report /// them once rather than at every reference. fn fsck_objects(&self, rep: &mut FsckReport) -> Result> { let mut bad = HashSet::new(); for r in self.store.list()? { rep.objects += 1; match self.store.get(&r) { Ok((ty, body)) => { if let Err(e) = decodes(ty, &body) { rep.error( Some(r), format!("{} object does not decode: {e}", ty.name()), ); bad.insert(r); } } Err(e) => { rep.error(Some(r), format!("unreadable object: {e}")); bad.insert(r); } } } Ok(bad) } /// Pass 2. fn fsck_ops(&self, rep: &mut FsckReport, bad: &HashSet) { let head = match fs::read_to_string(self.oxy.join("ops").join("head")) { Ok(h) => h, Err(e) => { rep.error(None, format!("cannot read .oxy/ops/head: {e}")); return; } }; let Some(head) = ObjectRef::from_hex(head.trim()) else { rep.error(None, ".oxy/ops/head does not hold an object reference"); return; }; let mut seen = HashSet::new(); let mut queue: VecDeque<(ObjectRef, Option<(ObjectRef, u64)>)> = VecDeque::from([(head, None)]); while let Some((r, child)) = queue.pop_front() { if !seen.insert(r) { continue; } if bad.contains(&r) { continue; } let body = match self.store.get_typed(&r, ObjectType::Op) { Ok(b) => b, Err(StoreError::NotFound(_)) => { let by = child .map(|(c, _)| format!(", the parent of operation {}", c.short(12))) .unwrap_or_default(); rep.error( Some(r), format!("operation missing{by}; the log is cut here"), ); continue; } Err(e) => { rep.error(Some(r), format!("operation unreadable: {e}")); continue; } }; rep.ops += 1; let (parents, wall_ms) = match Op::decode(&body) { Ok(op) => (op.parents, op.hlc.wall_ms), Err(_) => match Op::decode_format1(&body) { Ok(op) => (op.parents, op.hlc.wall_ms), Err(e) => { rep.error(Some(r), format!("operation does not decode: {e}")); continue; } }, }; if let Some((c, child_ms)) = child { if wall_ms > child_ms { rep.warn( Some(c), format!( "operation is timed before its parent {} (a clock was wrong)", r.short(12) ), ); } } queue.extend(parents.into_iter().map(|p| (p, Some((r, wall_ms))))); } } /// Pass 3. fn fsck_references(&self, rep: &mut FsckReport, bad: &HashSet) -> Result<()> { let view = match self.view() { Ok(v) => v, Err(e) => { rep.error(None, format!("cannot read the current view: {e}")); return Ok(()); } }; let boundary = self.boundary(); let partial = self.config.max_blob.is_some() || !self.config.sparse_paths.is_empty(); let mut roots: Vec<(ChangeHash, String)> = Vec::new(); let mut states: Vec<(ObjectRef, String)> = Vec::new(); states.extend( view.bookmarks .iter() .map(|(n, s)| (*s, format!("bookmark {n}"))), ); states.extend(view.tags.iter().map(|(n, s)| (*s, format!("tag {n}")))); for (remote, bms) in &view.remotes { states.extend(bms.iter().map(|(n, s)| (*s, format!("{remote}/{n}")))); } for (st, what) in states { match self.store.get_state(&st) { Ok(s) => roots.extend(s.heads.into_iter().map(|h| (h, what.clone()))), Err(e) if !bad.contains(&st) => { rep.error(Some(st), format!("state of {what} unusable: {e}")) } Err(_) => {} } } for (ws, h) in &view.workspaces { roots.push((*h, format!("workspace {}", ws.to_hex()))); } if let Some(p) = view.policy { match self.store.get_typed(&p, ObjectType::Policy) { Ok(bytes) => { if let Ok(policy) = crate::policy::Policy::decode(&bytes) { for id in &policy.identities { if !self.store.has(&id.hash()) { rep.warn( Some(id.hash()), "an identity the policy names is not stored", ); } } } } Err(e) if !bad.contains(&p) => { rep.error(Some(p), format!("the policy is unusable: {e}")) } Err(_) => {} } } // Working-copy changes are never signed; they are checked but not tallied. let working: HashSet = view.workspaces.values().copied().collect(); let mut seen: HashSet = HashSet::new(); let mut blobs_seen: HashSet = HashSet::new(); let mut stack = roots; while let Some((h, needed_by)) = stack.pop() { if !seen.insert(h) { continue; } // The skeleton: the text is checked as the blobs it is stored // in, not decoded (the objects pass checked every object). let (c, texts) = match self.store.get_change_lazy(&h) { Ok(c) => c, Err(StoreError::NotFound(_)) if boundary.contains(&h) => continue, Err(StoreError::NotFound(_)) => { rep.error(Some(h), format!("change missing, needed by {needed_by}")); continue; } Err(e) => { if !bad.contains(&h) { rep.error(Some(h), format!("change unusable: {e}")); } continue; } }; rep.changes += 1; let label = format!("change {}", h.short(12)); stack.extend(c.deps.iter().map(|d| (*d, label.clone()))); if !c.author.is_zero() && !self.store.has(&c.author) { rep.warn( Some(h), format!("author identity {} is not stored", c.author.short(12)), ); } for t in texts { if let crate::objects::TextRef::Blob(b, _, _) = t { if blobs_seen.insert(b) { // A change cannot be read without its text: never partial. self.fsck_blob(rep, b, &label, false, bad); } } } for e in &c.edits { let Edit::Replace { old, new, .. } = e else { continue; }; for b in [old, new].into_iter().flatten() { if !blobs_seen.insert(*b) { continue; } self.fsck_blob(rep, *b, &label, partial, bad); } } if working.contains(&h) { continue; } match self.authenticate_change(&h) { Ok(SignatureStatus::Valid { .. }) => rep.signed += 1, Ok(SignatureStatus::Unsigned) => rep.unsigned += 1, Ok(SignatureStatus::Invalid(why)) => { rep.error(Some(h), format!("bad signature: {why}")) } Err(e) => rep.error(Some(h), format!("cannot check signatures: {e}")), } } Ok(()) } fn fsck_blob( &self, rep: &mut FsckReport, b: ObjectRef, used_by: &str, partial: bool, bad: &HashSet, ) { match self.store.get_typed(&b, ObjectType::Blob) { Ok(bytes) => { let Ok(blob) = Blob::decode(&bytes) else { return; }; for ch in blob.chunks { if !self.store.has(&ch) { rep.error( Some(ch), format!("chunk missing from blob {} ({used_by})", b.short(12)), ); } } } Err(StoreError::NotFound(_)) if partial => rep.warn( Some(b), format!("blob not fetched ({used_by}); expected in a partial clone"), ), Err(StoreError::NotFound(_)) => rep.error(Some(b), format!("blob missing ({used_by})")), Err(e) if !bad.contains(&b) => { rep.error(Some(b), format!("blob unusable ({used_by}): {e}")) } Err(_) => {} } } /// Whether `heads` form a valid state: its history loads and every file /// graph builds. Graphs are built a batch at a time and dropped, so the /// check needs the memory of a batch, not of the whole tree. fn check_state(&self, heads: &[ChangeHash]) -> Result<()> { let mut st = self.load_state_history(heads)?; let nodes = st.unbuilt_nodes(); for batch in nodes.chunks(2048) { self.load_graphs_of(&mut st, batch)?; st.drop_graphs(batch); } Ok(()) } /// Pass 4. fn fsck_derived(&self, rep: &mut FsckReport) -> Result<()> { let Ok(view) = self.view() else { return Ok(()) }; let shallow = self.is_shallow(); if !shallow { let mut checked: HashSet = HashSet::new(); for (name, st) in &view.bookmarks { if !checked.insert(*st) { continue; } let Ok(s) = self.store.get_state(st) else { continue; }; if let Err(e) = self.check_state(&s.heads) { rep.error( Some(*st), format!("bookmark {name} does not form a valid state: {e}"), ); } } } for r in self.checkpoint_refs()? { rep.checkpoints += 1; let cp = match self.store.get_typed(&r, ObjectType::Checkpoint) { Ok(b) => match crate::objects::Checkpoint::decode(&b) { Ok(cp) => cp, Err(_) => continue, }, Err(StoreError::NotFound(_)) => { rep.warn( Some(r), "checkpoint pointer names a missing checkpoint; it will be rewritten", ); continue; } Err(_) => continue, }; let payload = match self.read_blob(&cp.payload) { Ok(p) => p, Err(e) => { rep.error(Some(r), format!("checkpoint payload unreadable: {e}")); continue; } }; if let Err(e) = self.state_from_checkpoint(&payload) { rep.error(Some(r), format!("checkpoint payload does not decode: {e}")); continue; } // The payload must be what the named changes produce, in its own version. let version = crate::cbor::decode(&payload).ok().and_then(|v| { crate::objects::map(&v, "payload").ok().and_then(|m| { m.get(&0) .and_then(|x| crate::objects::uint(x, "version").ok()) }) }); let expected = |full: &State| { if version == Some(crate::state::CHECKPOINT_INDEX_VERSION) { full.checkpoint_index(&full.checkpoint_file_graphs(), Repo::blob_ref) } else { full.checkpoint_payload() } }; if !shallow { match self.load_state_full(&cp.heads) { Ok(full) if expected(&full) != payload => { rep.error(Some(r), "checkpoint disagrees with the changes it names") } Ok(_) => {} Err(e) => rep.error( Some(r), format!("checkpoint names changes that do not form a state: {e}"), ), } } } let mut attests = Vec::new(); for name in view.bookmarks.keys() { if let Ok(Some((r, _))) = self.latest_ref_attest(name) { attests.push((r, format!("bookmark {name}"))); } } for name in view.tags.keys() { if let Ok(Some((r, _))) = self.tag_attest(name) { attests.push((r, format!("tag {name}"))); } } for (r, what) in attests { rep.attestations += 1; let sigs = self.store.signatures(&r)?; if sigs.is_empty() { rep.warn(Some(r), format!("attestation for {what} is unsigned")); } for sig in sigs { match self.store.get_identity(&sig.key) { Ok(id) => { if let Err(e) = verify_signature(&id, &sig, Domain::Attest, &r) { rep.error( Some(r), format!("attestation for {what} has a bad signature: {e}"), ); } } Err(_) => rep.warn( Some(r), format!( "attestation for {what} is signed by {}, whose identity is not stored", sig.key.short(12) ), ), } } } Ok(()) } }