Skip to main content

postgres_test_harness/
fingerprint.rs

1use std::fmt;
2
3use sha2::{Digest, Sha256};
4
5use crate::{Error, Result};
6
7const FINGERPRINT_BYTES: usize = 32;
8const SHORT_FINGERPRINT_HEX_LEN: usize = 24;
9
10/// SHA-256 identity for every input that shapes an initialized template.
11#[derive(Clone, Copy, Eq, Hash, PartialEq)]
12pub struct TemplateFingerprint([u8; FINGERPRINT_BYTES]);
13
14impl TemplateFingerprint {
15    pub fn from_hex(hex: &str) -> Result<Self> {
16        if hex.len() != FINGERPRINT_BYTES * 2 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
17            return Err(Error::InvalidTemplateFingerprint);
18        }
19        let mut bytes = [0_u8; FINGERPRINT_BYTES];
20        for (index, chunk) in hex.as_bytes().as_chunks::<2>().0.iter().enumerate() {
21            let encoded =
22                std::str::from_utf8(chunk).map_err(|_| Error::InvalidTemplateFingerprint)?;
23            bytes[index] =
24                u8::from_str_radix(encoded, 16).map_err(|_| Error::InvalidTemplateFingerprint)?;
25        }
26        Ok(Self(bytes))
27    }
28
29    pub fn to_hex(self) -> String {
30        let mut output = String::with_capacity(FINGERPRINT_BYTES * 2);
31        for byte in self.0 {
32            use std::fmt::Write as _;
33            write!(&mut output, "{byte:02x}").expect("writing to a String cannot fail");
34        }
35        output
36    }
37
38    pub(crate) fn short_hex(self) -> String {
39        self.to_hex()[..SHORT_FINGERPRINT_HEX_LEN].to_owned()
40    }
41}
42
43impl fmt::Debug for TemplateFingerprint {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        formatter
46            .debug_tuple("TemplateFingerprint")
47            .field(&self.to_hex())
48            .finish()
49    }
50}
51
52/// Length-framed fingerprint builder that avoids concatenation ambiguity.
53pub struct FingerprintBuilder(Sha256);
54
55impl FingerprintBuilder {
56    pub fn new(domain: impl AsRef<[u8]>) -> Self {
57        let mut builder = Self(Sha256::new());
58        builder.add_frame(b"postgres-test-harness-template-v1");
59        builder.add_frame(domain.as_ref());
60        builder
61    }
62
63    pub fn add(mut self, label: impl AsRef<[u8]>, content: impl AsRef<[u8]>) -> Self {
64        self.add_frame(label.as_ref());
65        self.add_frame(content.as_ref());
66        self
67    }
68
69    pub fn finish(self) -> TemplateFingerprint {
70        TemplateFingerprint(self.0.finalize().into())
71    }
72
73    fn add_frame(&mut self, bytes: &[u8]) {
74        self.0.update((bytes.len() as u64).to_be_bytes());
75        self.0.update(bytes);
76    }
77}
78
79/// Inputs required to locate or create one immutable database template.
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub struct TemplateSpec {
82    fingerprint: TemplateFingerprint,
83}
84
85impl TemplateSpec {
86    pub fn new(fingerprint: TemplateFingerprint) -> Self {
87        Self { fingerprint }
88    }
89
90    pub fn fingerprint(self) -> TemplateFingerprint {
91        self.fingerprint
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{FingerprintBuilder, TemplateFingerprint};
98
99    #[test]
100    fn fingerprint_frames_labels_and_content() {
101        let left = FingerprintBuilder::new("schema").add("ab", "c").finish();
102        let right = FingerprintBuilder::new("schema").add("a", "bc").finish();
103        assert_ne!(left, right);
104    }
105
106    #[test]
107    fn fingerprint_matches_known_vector_and_hex_round_trips() {
108        let fingerprint = FingerprintBuilder::new("schema")
109            .add("migration", "select 1")
110            .finish();
111        let expected = "fc8eb7daf3f83a0c92ccf0b5f4122bc72e6a84dba4eb38896c2c9d0ef95afe7f";
112
113        assert_eq!(fingerprint.to_hex(), expected);
114        assert_eq!(
115            TemplateFingerprint::from_hex(expected).unwrap(),
116            fingerprint
117        );
118    }
119}