1use sha2::{Digest, Sha256};
10use std::fmt::Write as _;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Pin {
15 pub contract: String,
17 pub digest_hex: String,
19}
20
21pub 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#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Disagreement {
56 pub expected: Pin,
58 pub observed_digest_hex: String,
60}
61
62pub 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}