Skip to main content

sinter_store/
snapshot.rs

1//! Deterministic identity of one committed graph snapshot.
2
3use redb::{ReadableMultimapTable, ReadableTable};
4
5use crate::error::StoreError;
6use crate::store::{FILE_HASH, FILE_SCOPE, FileStamp, META, NODES, OUT_EDGES, RESOLVE_META, Store};
7
8impl Store {
9    /// Stable token for the committed graph snapshot served by this store.
10    ///
11    /// Normal repository stores hash the schema, source content hashes, and
12    /// non-source resolution fingerprints. That makes harmless reads and stat
13    /// changes stable while any indexed source or compiler-input change moves
14    /// the token. Hand-built stores used by tests/export have no file hashes,
15    /// so they fall back to hashing their node and edge rows.
16    pub fn snapshot_token(&self) -> Result<String, StoreError> {
17        let txn = self.db.begin_read()?;
18        let mut fingerprint = SnapshotFingerprint::new();
19        let schema = match txn.open_table(META) {
20            Ok(table) => table.get("schema")?.map(|guard| guard.value()).unwrap_or(0),
21            Err(redb::TableError::TableDoesNotExist(_)) => 0,
22            Err(error) => return Err(error.into()),
23        };
24        fingerprint.field(b"schema");
25        fingerprint.field(&schema.to_le_bytes());
26
27        let hashes = txn.open_table(FILE_HASH)?;
28        let mut file_count = 0usize;
29        for entry in hashes.iter()? {
30            let (file, stamp) = entry?;
31            fingerprint.field(file.value().as_bytes());
32            fingerprint.field(FileStamp::decode(stamp.value()).hash.as_bytes());
33            file_count += 1;
34        }
35        drop(hashes);
36
37        let scopes = txn.open_table(FILE_SCOPE)?;
38        for entry in scopes.iter()? {
39            let (file, scope) = entry?;
40            fingerprint.field(file.value().as_bytes());
41            fingerprint.field(scope.value().as_bytes());
42        }
43        drop(scopes);
44
45        let resolution = txn.open_table(RESOLVE_META)?;
46        for entry in resolution.iter()? {
47            let (kind, value) = entry?;
48            fingerprint.field(kind.value().as_bytes());
49            fingerprint.field(value.value().as_bytes());
50        }
51        drop(resolution);
52
53        if file_count == 0 {
54            let nodes = txn.open_table(NODES)?;
55            for entry in nodes.iter()? {
56                let (id, bytes) = entry?;
57                fingerprint.field(id.value().as_bytes());
58                fingerprint.field(bytes.value());
59            }
60            drop(nodes);
61            let edges = txn.open_multimap_table(OUT_EDGES)?;
62            for entry in edges.iter()? {
63                let (id, values) = entry?;
64                fingerprint.field(id.value().as_bytes());
65                for value in values {
66                    fingerprint.field(value?.value());
67                }
68            }
69        }
70
71        Ok(format!(
72            "graph-v{schema}-{:016x}{:016x}",
73            fingerprint.left, fingerprint.right
74        ))
75    }
76}
77
78/// Two independent FNV-1a streams are sufficient for a deterministic
79/// precondition token without adding a hashing dependency to the store.
80/// Length framing prevents concatenation ambiguity; this is an identity
81/// checksum, not an adversarial cryptographic commitment.
82struct SnapshotFingerprint {
83    left: u64,
84    right: u64,
85}
86
87impl SnapshotFingerprint {
88    fn new() -> Self {
89        Self {
90            left: 0xcbf29ce484222325,
91            right: 0x6c62272e07bb0142,
92        }
93    }
94
95    fn field(&mut self, bytes: &[u8]) {
96        self.write(&(bytes.len() as u64).to_le_bytes());
97        self.write(bytes);
98    }
99
100    fn write(&mut self, bytes: &[u8]) {
101        for byte in bytes {
102            self.left ^= u64::from(*byte);
103            self.left = self.left.wrapping_mul(0x0000_0100_0000_01b3);
104            self.right ^= u64::from(*byte).rotate_left(1);
105            self.right = self.right.wrapping_mul(0x9e37_79b1_85eb_ca87);
106        }
107    }
108}