postgres_test_harness/
fingerprint.rs1use 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#[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
52pub 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#[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}