Skip to main content

devscout_rs/truth/
pin.rs

1//! The pinned foundation-candidate identity.
2//!
3//! A content digest over the manifest and fixture pack bytes, plus the
4//! contract version, that a consumer verifies offline. [`verify_pin`] has
5//! no mutating counterpart in this module -- a disagreeing consumer can
6//! only report a [`Disagreement`], never amend, back-fill, or re-derive
7//! the pinned expectation.
8
9use sha2::{Digest, Sha256};
10use std::fmt::Write as _;
11
12/// The pinned identity a consumer checks its own copy against.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Pin {
15    /// The contract version the digest was computed under.
16    pub contract: String,
17    /// The lowercase hex content digest.
18    pub digest_hex: String,
19}
20
21/// Computes the pin over the manifest and fixture-source bytes and the
22/// contract version.
23///
24/// Byte-order of the inputs is fixed by the caller (manifest first, then
25/// each fixture file in a stable, sorted order), so the same inputs
26/// always produce the same digest.
27pub fn compute_pin(contract: &str, manifest_bytes: &[u8], fixture_files: &[(&str, &[u8])]) -> Pin {
28    let mut hasher = Sha256::new();
29    hasher.update(contract.as_bytes());
30    hasher.update(b"\0");
31    hasher.update(manifest_bytes);
32    let mut sorted: Vec<&(&str, &[u8])> = fixture_files.iter().collect();
33    sorted.sort_by_key(|(path, _)| *path);
34    for (path, bytes) in sorted {
35        hasher.update(b"\0");
36        hasher.update(path.as_bytes());
37        hasher.update(b"\0");
38        hasher.update(bytes);
39    }
40    let digest = hasher.finalize();
41    let mut digest_hex = String::with_capacity(64);
42    for byte in digest {
43        let _ = write!(digest_hex, "{byte:02x}");
44    }
45    Pin {
46        contract: contract.to_string(),
47        digest_hex,
48    }
49}
50
51/// A consumer's own copy disagreed with the pin. This is the only outcome
52/// a disagreeing consumer can reach -- there is no path anywhere in this
53/// module from a `Disagreement` back into a rewritten `Pin`.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Disagreement {
56    /// The pin the consumer's copy was checked against.
57    pub expected: Pin,
58    /// The digest the consumer actually computed.
59    pub observed_digest_hex: String,
60}
61
62/// Confirms a consumer's own recomputed digest matches the pin, offline.
63/// Returns the mismatch as data on disagreement; nothing here can write a
64/// new pin.
65pub fn verify_pin(expected: &Pin, observed_digest_hex: &str) -> Result<(), Disagreement> {
66    if expected.digest_hex == observed_digest_hex {
67        Ok(())
68    } else {
69        Err(Disagreement {
70            expected: expected.clone(),
71            observed_digest_hex: observed_digest_hex.to_string(),
72        })
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn the_same_inputs_produce_the_same_pin() {
82        let a = compute_pin("semantic-truth-v1", b"{}", &[("src/A.cs", b"class A {}")]);
83        let b = compute_pin("semantic-truth-v1", b"{}", &[("src/A.cs", b"class A {}")]);
84        assert_eq!(a, b);
85    }
86
87    #[test]
88    fn fixture_file_order_does_not_change_the_pin() {
89        let a = compute_pin(
90            "semantic-truth-v1",
91            b"{}",
92            &[("src/A.cs", b"class A {}"), ("src/B.cs", b"class B {}")],
93        );
94        let b = compute_pin(
95            "semantic-truth-v1",
96            b"{}",
97            &[("src/B.cs", b"class B {}"), ("src/A.cs", b"class A {}")],
98        );
99        assert_eq!(
100            a, b,
101            "the digest sorts fixture files itself, independent of call order"
102        );
103    }
104
105    #[test]
106    fn a_disagreeing_consumer_output_is_reported_never_applied() {
107        let pin = compute_pin("semantic-truth-v1", b"{}", &[]);
108        let result = verify_pin(&pin, "not-the-real-digest");
109        assert_eq!(
110            result,
111            Err(Disagreement {
112                expected: pin,
113                observed_digest_hex: "not-the-real-digest".to_string(),
114            })
115        );
116    }
117
118    #[test]
119    fn a_matching_consumer_output_verifies() {
120        let pin = compute_pin("semantic-truth-v1", b"{}", &[]);
121        assert_eq!(verify_pin(&pin, &pin.digest_hex), Ok(()));
122    }
123}