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