godspeed_sdk/churn.rs
1// SPDX-License-Identifier: Apache-2.0
2//! The self-describing content pattern that makes a TORN FILE detectable.
3//!
4//! **Why this is in the SDK and not in whichever crate happens to write it.** This is a contract
5//! between a WRITER and a CHECKER, and its failure mode if the two ever disagree is the worst one
6//! available: `churn verify` reports "NONE torn" because it no longer recognises a tear. A safety
7//! check that passes because it stopped working is worse than no check, because somebody trusts it.
8//!
9//! The two halves used to be one expression written twice in `services/shell` - tolerable while both
10//! copies sat a few hundred lines apart in one file, where anybody editing one saw the other.
11//! Backgrounding `churn` puts the writer in `services/copier` and leaves the checker in the shell,
12//! in different crates that deliberately do not share headers. So the pattern moves here, where
13//! there is exactly one of it and both callers name the same function.
14//!
15//! **What the pattern is.** Every byte encodes the generation that wrote it:
16//!
17//! ```text
18//! byte[k] = (gen + k) mod 251
19//! ```
20//!
21//! A file written wholly in one generation satisfies that for every `k`. A file holding a MIX of two
22//! generations breaks it at exactly the byte where the tear happened, so the checker learns `gen`
23//! from byte 0 and then walks the rest.
24//!
25//! **251 is the largest prime under 256, and the primality is doing work.** A prime stride does not
26//! align with the 508-byte block payload, so a tear landing on a block boundary still lands
27//! mid-pattern and stays visible instead of reading as a continuation.
28//!
29//! This is the half that structural checking cannot reach: `drives check` validates the tree, the
30//! bitmap and the CRCs, and none of that can tell whether a file holds the first half of one write
31//! and the second half of another. The permitted-outcome table says a whole-file write must be
32//! old-complete or new-complete and never a mix; this is what makes that claim testable.
33
34/// The byte a file written in generation `gen` must hold at offset `k`.
35///
36/// Writer and checker MUST both come through here. Inlining the arithmetic at a call site is how
37/// the two drift.
38#[inline]
39pub fn byte_at(gen: u8, k: usize) -> u8 {
40 gen.wrapping_add((k % 251) as u8) % 251
41}
42
43/// The generation for the `i`th write of a churn run.
44#[inline]
45pub fn generation(i: u64) -> u8 {
46 (i % 251) as u8
47}
48
49/// Fill `buf` with one generation's content, end to end.
50#[inline]
51pub fn fill(buf: &mut [u8], gen: u8) {
52 for (k, b) in buf.iter_mut().enumerate() {
53 *b = byte_at(gen, k);
54 }
55}
56
57/// The first offset in `data` that does NOT match the generation recorded in its byte 0, if any.
58///
59/// `None` means every byte agrees: the file holds one generation end to end. `Some(k)` is the exact
60/// byte where two writes meet, which is what makes a tear report actionable rather than a suspicion.
61/// An empty slice has nothing to disagree with and is reported as intact - a file with no bytes is a
62/// separate condition and the caller counts it separately.
63#[inline]
64pub fn first_divergence(data: &[u8]) -> Option<usize> {
65 let gen = match data.first() {
66 Some(&g) => g,
67 None => return None,
68 };
69 data.iter().enumerate().find(|(k, &b)| b != byte_at(gen, *k)).map(|(k, _)| k)
70}