Skip to main content

blockworx_store/
dump.rs

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