1use 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
25pub struct Dump {
28 pub lines: Vec<String>,
29 pub broken: Option<miette::Report>,
32}
33
34pub 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
54pub struct Verification {
57 pub lines: Vec<String>,
58 pub broken: Option<miette::Report>,
59}
60
61pub 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 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
113fn 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
122fn 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
152pub 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 #[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 #[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 #[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}