1use blockworx_doc::{
20 id::EntityRef,
21 rev::Rev,
22 trail::{JournalAs, Trail},
23};
24use serde::{Deserialize, Serialize};
25
26use super::record::{Camera, Digest, Identity, ScopePath, WallTime};
27use super::tags::{Tagging, Tags};
28
29pub const TOUCHED: usize = 64;
33
34#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
41pub enum RowKind {
42 #[serde(rename = "edit")]
43 Edit,
44 #[serde(rename = "undo")]
45 Undo { of: Rev },
46 #[serde(rename = "redo")]
47 Redo { of: Rev },
48 #[serde(rename = "tag")]
52 Tag,
53 #[serde(rename = "untag")]
56 Untag,
57}
58
59#[derive(Clone, Copy, PartialEq, Eq, Debug)]
63pub enum Replays {
64 Trail(JournalAs),
66 Vocabulary(Tagging),
68}
69
70impl RowKind {
71 pub fn replays(self) -> Replays {
77 match self {
78 RowKind::Edit => Replays::Trail(JournalAs::Edit),
79 RowKind::Undo { of } => Replays::Trail(JournalAs::Undo { of }),
80 RowKind::Redo { of } => Replays::Trail(JournalAs::Redo { of }),
81 RowKind::Tag => Replays::Vocabulary(Tagging::Added),
82 RowKind::Untag => Replays::Vocabulary(Tagging::Removed),
83 }
84 }
85
86 pub fn takes_a_rev(self) -> bool {
89 matches!(self.replays(), Replays::Trail(_))
90 }
91
92 pub fn steps(self) -> Option<Rev> {
94 match self {
95 RowKind::Undo { of } | RowKind::Redo { of } => Some(of),
96 RowKind::Edit | RowKind::Tag | RowKind::Untag => None,
97 }
98 }
99}
100
101#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
103pub struct Row {
104 pub rev: Rev,
105 pub kind: RowKind,
106 pub wall_time: WallTime,
107 pub author: Identity,
108 pub label: String,
109 pub scope: ScopePath,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub scope_names: Vec<String>,
118 pub camera: Camera,
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub touched: Vec<EntityRef>,
123 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
125 pub truncated: bool,
126 pub hash: Digest,
130 pub parent: Digest,
133}
134
135impl Row {
136 #[expect(clippy::expect_used, clippy::missing_panics_doc)]
143 pub fn canonical_bytes(&self) -> Vec<u8> {
144 let value = serde_json::to_value(self).expect("a manifest row serializes infallibly");
145 let mut bytes = Vec::new();
146 write_canonical(&value, &mut bytes).expect("writing to a vector is infallible");
147 bytes
148 }
149
150 pub fn digest(&self) -> Digest {
152 Digest::of(&self.canonical_bytes())
153 }
154
155 #[expect(clippy::expect_used, clippy::missing_panics_doc)]
159 pub fn digest_as_written(&self, line: &str) -> Digest {
160 let canonical = self.canonical_bytes();
161 if canonical == line.as_bytes() {
162 return Digest::of(&canonical);
163 }
164 let value: serde_json::Value =
165 serde_json::from_str(line).expect("the line parsed as a row");
166 let mut bytes = Vec::new();
167 write_canonical(&value, &mut bytes).expect("writing to a vector is infallible");
168 Digest::of(&bytes)
169 }
170
171 pub fn naming(mut touched: Vec<EntityRef>) -> (Vec<EntityRef>, bool) {
173 let truncated = touched.len() > TOUCHED;
174 touched.truncate(TOUCHED);
175 (touched, truncated)
176 }
177}
178
179pub(crate) fn write_canonical(
185 value: &serde_json::Value,
186 out: &mut Vec<u8>,
187) -> serde_json::Result<()> {
188 match value {
189 serde_json::Value::Object(entries) => {
190 let mut keys: Vec<&String> = entries.keys().collect();
191 keys.sort_unstable();
192 out.push(b'{');
193 for (position, key) in keys.into_iter().enumerate() {
194 if position > 0 {
195 out.push(b',');
196 }
197 serde_json::to_writer(&mut *out, key)?;
198 out.push(b':');
199 write_canonical(&entries[key], out)?;
200 }
201 out.push(b'}');
202 }
203 serde_json::Value::Array(items) => {
204 out.push(b'[');
205 for (position, item) in items.iter().enumerate() {
206 if position > 0 {
207 out.push(b',');
208 }
209 write_canonical(item, out)?;
210 }
211 out.push(b']');
212 }
213 leaf => serde_json::to_writer(out, leaf)?,
214 }
215 Ok(())
216}
217
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub struct Located {
222 pub line: usize,
224 pub column: usize,
226 pub offset: usize,
228 pub len: usize,
230}
231
232impl std::fmt::Display for Located {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 write!(f, "line {}, column {}", self.line, self.column)
235 }
236}
237
238#[derive(Debug)]
240pub enum Fault {
241 Malformed(serde_json::Error),
242 Chained {
245 expected: Digest,
246 found: Digest,
247 },
248 Misnumbered {
250 expected: Rev,
251 found: Rev,
252 },
253 Unreached {
255 of: Rev,
256 },
257 Unwitnessed {
260 at: Rev,
261 why: String,
262 },
263}
264
265impl std::fmt::Display for Fault {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 match self {
268 Fault::Malformed(error) => write!(f, "the row does not parse: {error}"),
269 Fault::Chained { expected, found } => write!(
270 f,
271 "the row links to {found}, but the row before it hashes to {expected} — \
272 the manifest has been rewritten here",
273 ),
274 Fault::Misnumbered { expected, found } => write!(
275 f,
276 "the row claims rev {found}, but the manifest reaches {expected}",
277 found = found.get(),
278 expected = expected.get(),
279 ),
280 Fault::Unreached { of } => write!(
281 f,
282 "the row steps rev {of}, which no row before it holds",
283 of = of.get(),
284 ),
285 Fault::Unwitnessed { at, why } => write!(
286 f,
287 "the row names rev {at}, whose file this container cannot show: {why}",
288 at = at.get(),
289 ),
290 }
291 }
292}
293
294#[derive(Debug)]
298pub struct BreakReport {
299 pub at: Located,
300 pub fault: Fault,
301}
302
303impl std::fmt::Display for BreakReport {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 write!(f, "{}: {}", self.at, self.fault)
306 }
307}
308
309#[derive(Clone, Copy, Debug)]
314pub struct DroppedTail {
315 pub at: Located,
316 pub good_bytes: u64,
318}
319
320#[derive(Clone, Copy, Debug)]
323pub enum Tail {
324 Whole,
325 Dropped(DroppedTail),
326}
327
328pub struct Verified {
330 pub row: Row,
331 pub at: Located,
332 pub digest: Digest,
334}
335
336pub enum End {
338 Whole,
339 Truncated(DroppedTail),
340 Broken(BreakReport),
341}
342
343pub struct Scan {
346 pub rows: Vec<Verified>,
347 pub end: End,
348}
349
350pub struct History {
353 pub rows: Vec<Row>,
356 pub trail: Trail,
357 pub tags: Tags,
358 pub head: Digest,
360}
361
362impl History {
363 pub fn rev(&self) -> Rev {
366 self.rows.last().map_or(Rev::ZERO, |row| row.rev)
367 }
368
369 pub fn row(&self, rev: Rev) -> Option<&Row> {
370 let at = usize::try_from(rev.get().checked_sub(1)?).ok()?;
371 self.rows.get(at)
372 }
373}
374
375pub fn scan(text: &str) -> Scan {
381 let mut rows: Vec<Verified> = Vec::new();
382 let mut parent = Digest::genesis();
383 let mut reached = Rev::ZERO;
384 let mut offset = 0;
385
386 for (number, chunk) in text.split_inclusive('\n').enumerate() {
387 let line = chunk.strip_suffix('\n');
388 let body = line.unwrap_or(chunk);
389 let at = Located {
390 line: number + 1,
391 column: 1,
392 offset,
393 len: body.len(),
394 };
395 if line.is_none() {
396 return Scan {
397 rows,
398 end: End::Truncated(DroppedTail {
399 at,
400 good_bytes: offset as u64,
401 }),
402 };
403 }
404 offset += chunk.len();
405
406 let row: Row = match serde_json::from_str(body) {
407 Ok(row) => row,
408 Err(error) => {
409 let at = Located {
410 column: error.column().max(1),
411 ..at
412 };
413 return broken(rows, at, Fault::Malformed(error));
414 }
415 };
416 if row.parent != parent {
417 let fault = Fault::Chained {
418 expected: parent,
419 found: row.parent,
420 };
421 return broken(rows, at, fault);
422 }
423 if let Some(fault) = misplaced(&row, reached) {
424 return broken(rows, at, fault);
425 }
426 if row.kind.takes_a_rev() {
427 reached = row.rev;
428 }
429 parent = row.digest_as_written(body);
430 rows.push(Verified {
431 row,
432 at,
433 digest: parent,
434 });
435 }
436
437 Scan {
438 rows,
439 end: End::Whole,
440 }
441}
442
443fn misplaced(row: &Row, reached: Rev) -> Option<Fault> {
447 if let Some(of) = row.kind.steps()
448 && of > reached
449 {
450 return Some(Fault::Unreached { of });
451 }
452 if !row.kind.takes_a_rev() {
453 return (row.rev == Rev::ZERO || row.rev > reached).then_some(Fault::Misnumbered {
456 expected: reached,
457 found: row.rev,
458 });
459 }
460 let expected = reached.next();
461 (row.rev != expected).then_some(Fault::Misnumbered {
462 expected,
463 found: row.rev,
464 })
465}
466
467fn broken(rows: Vec<Verified>, at: Located, fault: Fault) -> Scan {
468 Scan {
469 rows,
470 end: End::Broken(BreakReport { at, fault }),
471 }
472}
473
474pub fn history(rows: &[Verified]) -> History {
481 let mut trail = Trail::default();
482 let mut tags = Tags::default();
483 let mut kept = Vec::new();
484 for verified in rows {
485 let row = &verified.row;
486 match row.kind.replays() {
487 Replays::Vocabulary(how) => tags.apply(row.rev, &row.label, how),
488 Replays::Trail(journal) => {
489 trail.record(row.rev, journal);
490 kept.push(row.clone());
491 }
492 }
493 }
494 History {
495 rows: kept,
496 trail,
497 tags,
498 head: rows.last().map_or_else(Digest::genesis, |last| last.digest),
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use blockworx_doc::fixtures::{block_id, rev};
506
507 fn row(rev: Rev, kind: RowKind, parent: Digest) -> Row {
508 Row {
509 rev,
510 kind,
511 wall_time: WallTime::from_unix_millis(1_756_000_000_000),
512 author: Identity::new("ada"),
513 label: "Added a block".to_owned(),
514 scope: ScopePath::default(),
515 scope_names: Vec::new(),
516 camera: Camera::UNSEEN,
517 touched: vec![EntityRef::Block(block_id(1))],
518 truncated: false,
519 hash: Digest::of(b"a rev file"),
520 parent,
521 }
522 }
523
524 fn chained(kinds: &[(Rev, RowKind)]) -> String {
526 let mut parent = Digest::genesis();
527 let mut text = String::new();
528 for (at, kind) in kinds {
529 let row = row(*at, *kind, parent);
530 parent = row.digest();
531 text.push_str(&String::from_utf8(row.canonical_bytes()).expect("utf-8"));
532 text.push('\n');
533 }
534 text
535 }
536
537 #[test]
538 fn a_row_round_trips_through_its_own_canonical_bytes() {
539 let written = row(rev(1), RowKind::Edit, Digest::genesis());
540 let bytes = written.canonical_bytes();
541 let parsed: Row = serde_json::from_slice(&bytes).expect("the row parses back");
542 assert_eq!(parsed, written);
543 assert_eq!(
544 parsed.canonical_bytes(),
545 bytes,
546 "and re-canonicalizes to the same bytes, which is what the chain rests on",
547 );
548 }
549
550 #[test]
553 fn a_reformatted_line_links_exactly_as_the_line_it_reformats() {
554 let written = row(rev(1), RowKind::Edit, Digest::genesis());
555 let canonical = String::from_utf8(written.canonical_bytes()).expect("utf-8");
556 let pretty = serde_json::to_string_pretty(&written).expect("it re-serializes");
557 assert_ne!(pretty, canonical, "precondition: different bytes");
558 assert_eq!(
559 written.digest_as_written(&pretty),
560 written.digest_as_written(&canonical),
561 );
562 assert_eq!(written.digest_as_written(&canonical), written.digest());
563 }
564
565 #[test]
567 fn row_kinds_are_spelled_as_the_format_names_them() {
568 let spelling = |kind: RowKind| serde_json::to_string(&kind).expect("a kind serializes");
569 assert_eq!(spelling(RowKind::Edit), "\"edit\"");
570 assert_eq!(
571 spelling(RowKind::Undo { of: rev(3) }),
572 r#"{"undo":{"of":3}}"#
573 );
574 assert_eq!(
575 spelling(RowKind::Redo { of: rev(4) }),
576 r#"{"redo":{"of":4}}"#
577 );
578 assert_eq!(spelling(RowKind::Tag), "\"tag\"");
579 assert_eq!(spelling(RowKind::Untag), "\"untag\"");
580 for kind in [RowKind::Tag, RowKind::Untag] {
581 assert!(!kind.takes_a_rev(), "a tag row moves no stack: {kind:?}");
582 }
583 }
584
585 #[test]
588 fn rows_replay_into_the_trail_and_the_tags() {
589 let text = chained(&[
590 (rev(1), RowKind::Edit),
591 (rev(2), RowKind::Edit),
592 (rev(2), RowKind::Tag),
593 (rev(3), RowKind::Undo { of: rev(2) }),
594 ]);
595 let scanned = scan(&text);
596 assert!(matches!(scanned.end, End::Whole), "the manifest chains");
597
598 let history = history(&scanned.rows);
599 assert_eq!(history.rev(), rev(3));
600 assert_eq!(history.rows.len(), 3, "a tag consumes no rev");
601 assert_eq!(history.tags.of(rev(2)), ["Added a block"]);
602 assert_eq!(history.trail.standing(), rev(1), "the undo stepped back");
603 assert_eq!(history.trail.next_redo(), Some(rev(3)));
604 assert_eq!(history.row(rev(2)).map(|row| row.rev), Some(rev(2)));
605 }
606
607 #[test]
608 fn a_rewritten_row_breaks_the_chain_at_its_successor() {
609 let text = chained(&[(rev(1), RowKind::Edit), (rev(2), RowKind::Edit)]);
610 let mut lines: Vec<String> = text.lines().map(str::to_owned).collect();
611 lines[0] = lines[0].replace("Added a block", "Added a bloke");
612 let scanned = scan(&(lines.join("\n") + "\n"));
613
614 assert_eq!(scanned.rows.len(), 1, "the prefix before the break stands");
615 let End::Broken(report) = scanned.end else {
616 panic!("a rewritten row was accepted");
617 };
618 assert!(matches!(report.fault, Fault::Chained { .. }));
619 assert_eq!(report.at.line, 2, "the successor is what detects it");
620 }
621
622 #[test]
623 fn a_step_naming_a_rev_no_row_holds_is_refused() {
624 let text = chained(&[
625 (rev(1), RowKind::Edit),
626 (rev(2), RowKind::Undo { of: rev(9) }),
627 ]);
628 let End::Broken(report) = scan(&text).end else {
629 panic!("a step into nothing was accepted");
630 };
631 assert!(
632 matches!(report.fault, Fault::Unreached { of } if of == rev(9)),
633 "{}",
634 report.fault,
635 );
636 }
637
638 #[test]
639 fn a_row_out_of_sequence_is_refused() {
640 let text = chained(&[(rev(1), RowKind::Edit), (rev(3), RowKind::Edit)]);
641 let End::Broken(report) = scan(&text).end else {
642 panic!("a gap in the revs was accepted");
643 };
644 assert!(matches!(report.fault, Fault::Misnumbered { .. }));
645
646 let unheld = chained(&[(rev(1), RowKind::Edit), (rev(4), RowKind::Tag)]);
647 let End::Broken(report) = scan(&unheld).end else {
648 panic!("a tag on a rev the manifest does not hold was accepted");
649 };
650 assert!(matches!(report.fault, Fault::Misnumbered { .. }));
651 }
652
653 #[test]
657 fn a_partial_trailing_line_is_dropped_and_the_file_cut_back() {
658 let whole = chained(&[(rev(1), RowKind::Edit), (rev(2), RowKind::Edit)]);
659 let cut = whole.len() - 20;
660 let scanned = scan(&whole[..cut]);
661 assert_eq!(scanned.rows.len(), 1);
662 let End::Truncated(dropped) = scanned.end else {
663 panic!("a half-written row was taken");
664 };
665 assert_eq!(
666 dropped.good_bytes as usize,
667 whole.lines().next().expect("a first line").len() + 1,
668 );
669 }
670
671 #[test]
673 fn a_touched_list_past_the_cap_is_cut_and_marked() {
674 let many: Vec<EntityRef> = (1..=200).map(|n| EntityRef::Block(block_id(n))).collect();
675 let (cut, truncated) = Row::naming(many);
676 assert_eq!(cut.len(), TOUCHED);
677 assert!(truncated);
678 assert_eq!(cut[0], EntityRef::Block(block_id(1)), "the cut is a prefix");
679
680 let (few, truncated) = Row::naming(vec![EntityRef::Document]);
681 assert_eq!(few, [EntityRef::Document]);
682 assert!(!truncated);
683 }
684
685 #[test]
688 fn a_row_carries_no_document_values() {
689 let written = row(rev(1), RowKind::Edit, Digest::genesis());
690 let text = String::from_utf8(written.canonical_bytes()).expect("utf-8");
691 let fields: serde_json::Map<String, serde_json::Value> =
692 serde_json::from_str(&text).expect("the row parses as an object");
693 let mut keys: Vec<&str> = fields.keys().map(String::as_str).collect();
694 keys.sort_unstable();
695 assert_eq!(
696 keys,
697 [
698 "author",
699 "camera",
700 "hash",
701 "kind",
702 "label",
703 "parent",
704 "rev",
705 "scope",
706 "touched",
707 "wall_time",
708 ],
709 );
710 assert_eq!(
711 fields["touched"],
712 serde_json::json!(["block b1"]),
713 "the index is names, not values",
714 );
715 }
716}