Skip to main content

blockworx/store/
dump.rs

1//! `blockworx log` and `blockworx verify`: a container's audit trail on
2//! the console (F7), and the fsck that proves it whole.
3//!
4//! Deliberately not a [`Store`](super::handle::Store): both read, so
5//! neither takes the container's lock nor repairs a crash-truncated
6//! tail. They read the file's own bytes through the same scanner the
7//! editor opens with, and report a manifest that stops verifying the way
8//! the editor does — the trail up to the break, then the break, with the
9//! line it happened on.
10//!
11//! What separates them is depth. `log` reads history, so it checks the
12//! chain and nothing else; `verify` hashes **every** rev file against the
13//! row that names it, which is the only thing that names a damaged rev
14//! exactly.
15
16use std::path::Path;
17
18use blockworx_doc::rev::Rev;
19
20use super::container::{Container, MANIFEST};
21use super::history::{self, Journal};
22use super::manifest::{self, BreakReport, End};
23use super::revs;
24
25/// A container's trail as text, and — when the manifest stops verifying —
26/// the break the trail ends at.
27pub struct Dump {
28    pub lines: Vec<String>,
29    /// The typed failure, already carrying the manifest's source, so the
30    /// console shows the offending row under an arrow.
31    pub broken: Option<miette::Report>,
32}
33
34/// Read `root`'s manifest and render it.
35///
36/// # Errors
37/// The read failure — there is no container at `root`, or its manifest
38/// cannot be read.
39pub fn dump(root: &Path) -> std::io::Result<Dump> {
40    let path = root.join(MANIFEST);
41    let text = std::fs::read_to_string(&path)?;
42    let scanned = manifest::scan(&text);
43    let broken = ended(&path, &text, &scanned.end);
44    let history = manifest::history(&scanned.rows);
45    Ok(Dump {
46        lines: history::lines(&history::rows(
47            Journal::Recorded(&history.rows),
48            &history.tags,
49        )),
50        broken,
51    })
52}
53
54/// What a full verification covered, and — when something does not hold
55/// up — the first thing that did not.
56pub struct Verification {
57    pub lines: Vec<String>,
58    pub broken: Option<miette::Report>,
59}
60
61/// Recompute every chain link in `root`'s manifest, and hash every rev
62/// file against the row that names it.
63///
64/// # Errors
65/// The read failure — there is no container at `root`, or its manifest
66/// cannot be read.
67pub fn verify(root: &Path) -> std::io::Result<Verification> {
68    let path = root.join(MANIFEST);
69    let text = std::fs::read_to_string(&path)?;
70    let written = text.lines().count();
71    let named = path.display().to_string();
72    let scanned = manifest::scan(&text);
73    let verified = scanned.rows.len();
74    let ended = ended(&path, &text, &scanned.end);
75    let history = manifest::history(&scanned.rows);
76    if let End::Broken(_) = scanned.end {
77        return Ok(Verification {
78            lines: vec![format!(
79                "{named}: {verified} of {written} rows verified before the break",
80            )],
81            broken: ended,
82        });
83    }
84    let mut lines = vec![
85        format!("{named}: {verified} of {written} rows verified — every chain link"),
86        format!("head {}", history.head),
87    ];
88    let backing = revs::Dir::at(root);
89    let unwitnessed = history
90        .rows
91        .iter()
92        .find_map(|row| revs::witnessed(&backing, row.rev, row.hash).err());
93    match &unwitnessed {
94        None => lines.push(format!(
95            "{revs} rev files hold the bytes their rows name",
96            revs = history.rows.len(),
97        )),
98        Some(fault) => lines.push(format!("{fault}")),
99    }
100    // A rev file the container cannot decode is a finding too: the row
101    // chain and the bytes can both be intact over a document this build
102    // no longer reads.
103    let unreadable = missing_artwork(root, &history.rows);
104    let said = unwitnessed
105        .map(|fault| miette::miette!("{fault}"))
106        .or_else(|| unreadable.map(|fault| miette::miette!("{fault}")));
107    Ok(Verification {
108        lines,
109        broken: said.or(ended),
110    })
111}
112
113/// The first rev whose document, artwork and all, the container cannot
114/// hand back.
115fn missing_artwork(root: &Path, rows: &[manifest::Row]) -> Option<revs::RevFault> {
116    let container = Container::reading(root).ok()?;
117    rows.iter()
118        .map(|row| row.rev)
119        .find_map(|at| container.read_rev(at).err())
120}
121
122/// How the row sequence stopped, as a located report — or nothing, for a
123/// manifest that ends where it should.
124fn ended(path: &Path, text: &str, end: &End) -> Option<miette::Report> {
125    match end {
126        End::Whole => None,
127        End::Truncated(dropped) => Some(report(
128            path,
129            text,
130            dropped.at.offset,
131            text.len() - dropped.at.offset,
132            "the manifest ends in a row that was never finished; the editor drops it \
133             on the next open",
134        )),
135        End::Broken(BreakReport { at, fault }) => {
136            Some(report(path, text, at.offset, at.len, &fault.to_string()))
137        }
138    }
139}
140
141fn report(path: &Path, text: &str, offset: usize, len: usize, said: &str) -> miette::Report {
142    miette::miette!(
143        labels = vec![miette::LabeledSpan::at(offset..offset + len, "here")],
144        "{said}",
145    )
146    .with_source_code(miette::NamedSource::new(
147        path.display().to_string(),
148        text.to_owned(),
149    ))
150}
151
152/// Whether a rev this history holds can be read back at all — what an
153/// undo to it would need.
154pub fn reachable(root: &Path, at: Rev) -> bool {
155    Container::reading(root).is_ok_and(|container| container.read_rev(at).is_ok())
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::store::handle::{Clock, Store};
162    use crate::store::record::WallTime;
163    use crate::store::tests::fixture;
164    use blockworx_doc::fixtures::rev;
165
166    fn pinned() -> Clock {
167        Clock::Pinned {
168            at: WallTime::from_unix_millis(1_756_000_000_000),
169            step: std::time::Duration::from_secs(1),
170        }
171    }
172
173    fn three_edits(root: &Path) {
174        let mut store = Store::create(root, pinned()).expect("the container");
175        for commit in fixture::edits(3) {
176            store
177                .submit_edit(commit, &fixture::author())
178                .expect("the edit lands");
179        }
180    }
181
182    #[test]
183    fn a_whole_manifest_dumps_every_row_and_reports_nothing() {
184        let dir = fixture::dir("dump-whole");
185        let root = dir.join("doc.bwx");
186        three_edits(&root);
187
188        let dumped = dump(&root).expect("the manifest reads");
189        assert_eq!(dumped.lines.len(), 3);
190        assert!(dumped.broken.is_none());
191        assert!(dumped.lines[0].starts_with("r1  "), "{}", dumped.lines[0]);
192        assert!(
193            dumped.lines[2].contains("Added a block"),
194            "{}",
195            dumped.lines[2]
196        );
197    }
198
199    /// The one behaviour that makes the dump worth running on a container
200    /// the editor already refused: the trail up to the break is real, and
201    /// is printed before the break is named.
202    #[test]
203    fn a_broken_manifest_dumps_its_verified_prefix_and_then_the_break() {
204        let dir = fixture::dir("dump-broken");
205        let root = dir.join("doc.bwx");
206        three_edits(&root);
207
208        let mut lines = crate::store::tests::manifest_lines(&root);
209        lines[1] = r#"{"rev":2,"kind":"edit","#.to_owned();
210        crate::store::tests::write_manifest(&root, &lines);
211
212        let dumped = dump(&root).expect("a damaged manifest still reads");
213        assert_eq!(
214            dumped.lines.len(),
215            1,
216            "the prefix that verified is what there is to show",
217        );
218        let said = format!("{:?}", dumped.broken.expect("the break is reported"));
219        assert!(said.contains("does not parse"), "{said}");
220        assert!(
221            said.contains("2 │"),
222            "the report points at the line: {said}"
223        );
224    }
225
226    /// The fsck's own job: a rev file that is not the bytes its row names
227    /// is found by rev, and the manifest chaining is no defence.
228    #[test]
229    fn verify_names_a_rev_file_that_is_not_what_its_row_says() {
230        let dir = fixture::dir("verify-rev-drift");
231        let root = dir.join("doc.bwx");
232        three_edits(&root);
233        assert!(
234            verify(&root).expect("the manifest reads").broken.is_none(),
235            "precondition: the container verifies before it is tampered with",
236        );
237
238        std::fs::write(revs::path(&root, rev(2)), b"not a document")
239            .expect("the rev file is rewritten");
240
241        let checked = verify(&root).expect("the manifest reads");
242        let said = format!("{:?}", checked.broken.expect("the damage is reported"));
243        assert!(said.contains("rev 2"), "{said}");
244        assert!(
245            checked.lines[0].contains("3 of 3 rows verified"),
246            "the manifest itself is still whole: {}",
247            checked.lines[0],
248        );
249        assert!(
250            !reachable(&root, rev(2)),
251            "an undo to a rev the container cannot read must refuse",
252        );
253    }
254
255    /// A rev that is simply gone is the same finding, and the same
256    /// refusal — the case P3's backfill used to repair and P5 has nothing
257    /// to repair from.
258    #[test]
259    fn verify_names_a_rev_file_that_is_missing() {
260        let dir = fixture::dir("verify-rev-missing");
261        let root = dir.join("doc.bwx");
262        three_edits(&root);
263        std::fs::remove_file(revs::path(&root, rev(2))).expect("the rev file goes");
264
265        let checked = verify(&root).expect("the manifest reads");
266        let said = format!("{:?}", checked.broken.expect("the hole is reported"));
267        assert!(said.contains("rev 2"), "{said}");
268        assert!(!reachable(&root, rev(2)));
269        assert!(
270            reachable(&root, rev(3)),
271            "the head is still readable, so the container still opens",
272        );
273    }
274
275    #[test]
276    fn a_path_that_is_not_a_container_says_so() {
277        let dir = fixture::dir("dump-missing");
278        assert!(dump(&dir.join("nothing-here")).is_err());
279        assert!(verify(&dir.join("nothing-here")).is_err());
280    }
281
282    #[test]
283    fn a_whole_manifest_verifies_end_to_end_and_says_what_it_covered() {
284        let dir = fixture::dir("verify-whole");
285        let root = dir.join("doc.bwx");
286        three_edits(&root);
287
288        let checked = verify(&root).expect("the manifest reads");
289        assert!(checked.broken.is_none(), "{:?}", checked.broken);
290        assert!(
291            checked.lines[0].contains("3 of 3 rows verified"),
292            "{}",
293            checked.lines[0],
294        );
295        assert!(
296            checked.lines[1].starts_with("head "),
297            "{}",
298            checked.lines[1]
299        );
300        assert!(
301            checked.lines[2].contains("3 rev files"),
302            "{}",
303            checked.lines[2],
304        );
305    }
306}