//! Encoding a whole document as one update, and checking that the //! result is the document it claims to be. //! //! The call below is `encode_state_as_update_v1` against an EMPTY state //! vector, and which call this is matters more than anything else in //! this file. Against the empty vector it encodes every block, the //! entire delete set, and — through yrs's own `merge_pending_v1` — the //! pending queue, meaning updates whose dependencies never arrived and //! which `apply_update` accepted and parked. That makes the result //! merge-equivalent to the log it replaces, not merely //! render-equivalent: every clock range and every tombstone survives, //! so a later concurrent merge integrates against a snapshot exactly as //! it would against the full history. //! //! Two neighbours are traps, and both look like this one: //! //! - `encode_diff_v1(&StateVector::default())` silently drops the //! pending queue (its own doc comment says so). A document holding an //! update whose dependency arrives later heals under a full replay and //! does NOT heal under a diff-built snapshot — and no render //! comparison can see the difference, because pending content is //! invisible in both documents. //! - yrs's own `Snapshot` type and `encode_state_from_snapshot` are a //! different feature (historical views, requiring `skip_gc`) that //! happens to share this module's name. use yrs::updates::decoder::Decode; use yrs::{Doc, Options, ReadTxn, StateVector, Transact}; /// The whole document, as one update. pub fn snapshot_bytes(doc: &Doc) -> Vec { doc.transact() .encode_state_as_update_v1(&StateVector::default()) } /// True when `candidate`, replayed into an empty document, is the same /// document as `doc`. /// /// Three comparisons, none of them on encoded bytes: update encoding is /// not canonical (merge boundaries vary), so byte equality would report /// false mismatches and no cheap byte-level proof exists. State vector /// equality says both documents saw the same clock ranges from the same /// clients; delete set equality says they agree on every deletion; /// `fragment_json` equality says they render to the same structure, /// which catches formatting-only divergence that a text comparison /// misses at identical cost. /// /// This is deliberately not a proof. What survives all three is "yrs /// corrupted content while preserving every clock range, every deletion /// and the full structural render", and that is a narrow enough residual /// to accept. /// /// What it CANNOT catch, stated because the temptation is to believe it /// can: a snapshot taken from a stale state verifies perfectly, because /// it is compared against the very document that produced it. That /// failure is closed by taking the snapshot from the replayed state /// (the spec's D3), not here. /// /// Nor can it catch `snapshot_bytes` being built from the wrong call. /// `ReadTxn::snapshot()` reads only `Store::blocks` — `pending` and /// `pending_ds` are separate `Store` fields it never touches — and /// `fragment_json` cannot render pending content either, so a candidate /// built by `encode_diff_v1` (which drops the pending queue; see this /// module's header) verifies as a perfect match against a document that /// is still holding data the candidate lost. This verifier is exactly /// as blind to that substitution as a render comparison would be. What /// actually guards it is `core/tests/snapshot.rs`'s /// `a_snapshot_with_a_parked_update_heals_once_its_dependency_arrives`, /// which parks an update, snapshots around it, and asserts the replay /// heals once the dependency arrives — a property only the correct call /// has. /// /// guid/client_id are the caller's, never generated here — the clippy /// disallowed-methods rule against drawing entropy from the CRDT library /// applies to this throwaway document exactly as it does to a real one. /// /// Note on the state-vector/delete-set comparison: `doc.transact()` /// returns the read-only `Transaction`, which — unlike `TransactionMut` /// — has no `delete_set()` of its own (that method tracks only the /// current transaction's deletions, not the document's whole history). /// `ReadTxn::snapshot()` is what exposes the document-wide state vector /// and delete set together (as `Snapshot { state_map, delete_set }`) on /// either transaction kind, so both comparisons go through it. pub fn verify_snapshot(doc: &Doc, candidate: &[u8], guid: &str, client_id: u64) -> bool { let update = match yrs::Update::decode_v1(candidate) { Ok(u) => u, Err(_) => return false, }; let rebuilt = Doc::with_options(Options::with_guid_and_client_id(guid.into(), client_id)); { let mut txn = rebuilt.transact_mut(); if txn.apply_update(update).is_err() { return false; } } // The state-vector/delete-set comparison is scoped to its own block // so both read-only transactions (`a`, `b`) are dropped before // `fragment_json` runs below. The hazard is not two overlapping // reads — yrs's own docs say concurrent readers are fine, and // that's not what this is anyway. `fragment_json` calls `root(doc)` // (`get_or_insert_xml_fragment`) BEFORE it opens its own read // transaction, and that helper goes through `doc.transact_mut()` — // a WRITE acquisition — internally. Calling `fragment_json(doc)` // while `a` (a read guard from `doc.transact()`) is still alive // means that write acquisition blocks on the outstanding reader on // this same thread, and since `a` is never going to be dropped // while the very call it's blocking on hasn't returned, the thread // deadlocks itself. Dropping `a`/`b` first, before calling // `fragment_json` on either document, is what avoids it — the rule // is "never hold a transaction guard across a call that opens one // of its own," not "never overlap two reads." let same_state = { let (a, b) = (doc.transact(), rebuilt.transact()); let (a_snapshot, b_snapshot) = (a.snapshot(), b.snapshot()); a_snapshot.state_map == b_snapshot.state_map && a_snapshot.delete_set == b_snapshot.delete_set }; same_state && crate::fragment_json(doc) == crate::fragment_json(&rebuilt) }