1use 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
24pub struct Dump {
27 pub lines: Vec<String>,
28 pub broken: Option<miette::Report>,
31}
32
33pub 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
53pub struct Verification {
56 pub lines: Vec<String>,
57 pub broken: Option<miette::Report>,
58}
59
60pub 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 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 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
113fn 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
121fn 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
132fn 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
159pub 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 #[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 #[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 #[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}