oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1//! Encoding a whole document as one update, and checking that the
2//! result is the document it claims to be.
3//!
4//! The call below is `encode_state_as_update_v1` against an EMPTY state
5//! vector, and which call this is matters more than anything else in
6//! this file. Against the empty vector it encodes every block, the
7//! entire delete set, and — through yrs's own `merge_pending_v1` — the
8//! pending queue, meaning updates whose dependencies never arrived and
9//! which `apply_update` accepted and parked. That makes the result
10//! merge-equivalent to the log it replaces, not merely
11//! render-equivalent: every clock range and every tombstone survives,
12//! so a later concurrent merge integrates against a snapshot exactly as
13//! it would against the full history.
14//!
15//! Two neighbours are traps, and both look like this one:
16//!
17//! - `encode_diff_v1(&StateVector::default())` silently drops the
18//! pending queue (its own doc comment says so). A document holding an
19//! update whose dependency arrives later heals under a full replay and
20//! does NOT heal under a diff-built snapshot — and no render
21//! comparison can see the difference, because pending content is
22//! invisible in both documents.
23//! - yrs's own `Snapshot` type and `encode_state_from_snapshot` are a
24//! different feature (historical views, requiring `skip_gc`) that
25//! happens to share this module's name.
26use yrs::updates::decoder::Decode;
27use yrs::{Doc, Options, ReadTxn, StateVector, Transact};
28
29/// The whole document, as one update.
30pub fn snapshot_bytes(doc: &Doc) -> Vec<u8> {
31 doc.transact()
32 .encode_state_as_update_v1(&StateVector::default())
33}
34
35/// True when `candidate`, replayed into an empty document, is the same
36/// document as `doc`.
37///
38/// Three comparisons, none of them on encoded bytes: update encoding is
39/// not canonical (merge boundaries vary), so byte equality would report
40/// false mismatches and no cheap byte-level proof exists. State vector
41/// equality says both documents saw the same clock ranges from the same
42/// clients; delete set equality says they agree on every deletion;
43/// `fragment_json` equality says they render to the same structure,
44/// which catches formatting-only divergence that a text comparison
45/// misses at identical cost.
46///
47/// This is deliberately not a proof. What survives all three is "yrs
48/// corrupted content while preserving every clock range, every deletion
49/// and the full structural render", and that is a narrow enough residual
50/// to accept.
51///
52/// What it CANNOT catch, stated because the temptation is to believe it
53/// can: a snapshot taken from a stale state verifies perfectly, because
54/// it is compared against the very document that produced it. That
55/// failure is closed by taking the snapshot from the replayed state
56/// (the spec's D3), not here.
57///
58/// Nor can it catch `snapshot_bytes` being built from the wrong call.
59/// `ReadTxn::snapshot()` reads only `Store::blocks` — `pending` and
60/// `pending_ds` are separate `Store` fields it never touches — and
61/// `fragment_json` cannot render pending content either, so a candidate
62/// built by `encode_diff_v1` (which drops the pending queue; see this
63/// module's header) verifies as a perfect match against a document that
64/// is still holding data the candidate lost. This verifier is exactly
65/// as blind to that substitution as a render comparison would be. What
66/// actually guards it is `core/tests/snapshot.rs`'s
67/// `a_snapshot_with_a_parked_update_heals_once_its_dependency_arrives`,
68/// which parks an update, snapshots around it, and asserts the replay
69/// heals once the dependency arrives — a property only the correct call
70/// has.
71///
72/// guid/client_id are the caller's, never generated here — the clippy
73/// disallowed-methods rule against drawing entropy from the CRDT library
74/// applies to this throwaway document exactly as it does to a real one.
75///
76/// Note on the state-vector/delete-set comparison: `doc.transact()`
77/// returns the read-only `Transaction`, which — unlike `TransactionMut`
78/// — has no `delete_set()` of its own (that method tracks only the
79/// current transaction's deletions, not the document's whole history).
80/// `ReadTxn::snapshot()` is what exposes the document-wide state vector
81/// and delete set together (as `Snapshot { state_map, delete_set }`) on
82/// either transaction kind, so both comparisons go through it.
83pub fn verify_snapshot(doc: &Doc, candidate: &[u8], guid: &str, client_id: u64) -> bool {
84 let update = match yrs::Update::decode_v1(candidate) {
85 Ok(u) => u,
86 Err(_) => return false,
87 };
88 let rebuilt = Doc::with_options(Options::with_guid_and_client_id(guid.into(), client_id));
89 {
90 let mut txn = rebuilt.transact_mut();
91 if txn.apply_update(update).is_err() {
92 return false;
93 }
94 }
95 // The state-vector/delete-set comparison is scoped to its own block
96 // so both read-only transactions (`a`, `b`) are dropped before
97 // `fragment_json` runs below. The hazard is not two overlapping
98 // reads — yrs's own docs say concurrent readers are fine, and
99 // that's not what this is anyway. `fragment_json` calls `root(doc)`
100 // (`get_or_insert_xml_fragment`) BEFORE it opens its own read
101 // transaction, and that helper goes through `doc.transact_mut()` —
102 // a WRITE acquisition — internally. Calling `fragment_json(doc)`
103 // while `a` (a read guard from `doc.transact()`) is still alive
104 // means that write acquisition blocks on the outstanding reader on
105 // this same thread, and since `a` is never going to be dropped
106 // while the very call it's blocking on hasn't returned, the thread
107 // deadlocks itself. Dropping `a`/`b` first, before calling
108 // `fragment_json` on either document, is what avoids it — the rule
109 // is "never hold a transaction guard across a call that opens one
110 // of its own," not "never overlap two reads."
111 let same_state = {
112 let (a, b) = (doc.transact(), rebuilt.transact());
113 let (a_snapshot, b_snapshot) = (a.snapshot(), b.snapshot());
114 a_snapshot.state_map == b_snapshot.state_map
115 && a_snapshot.delete_set == b_snapshot.delete_set
116 };
117 same_state && crate::fragment_json(doc) == crate::fragment_json(&rebuilt)
118}
119