Skip to main content

clankerdiff_fingerprint/
lib.rs

1//! Length-prefixed BLAKE3 fingerprints with a stable hexadecimal form.
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::fmt;
5
6const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
7const SOURCE_SEQUENCE_DOMAIN: &[u8] = b"diff-source-sequence-v1";
8
9/// Snapshot-local identity for one ordered sequence of source lines.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct SourceSequenceId(Fingerprint);
12
13impl SourceSequenceId {
14    #[must_use]
15    pub fn from_lines<'a>(lines: impl IntoIterator<Item = &'a str>) -> Self {
16        let fields =
17            std::iter::once(SOURCE_SEQUENCE_DOMAIN).chain(lines.into_iter().map(str::as_bytes));
18        Self(Fingerprint::of(fields))
19    }
20
21    #[must_use]
22    pub const fn fingerprint(self) -> Fingerprint {
23        self.0
24    }
25}
26
27impl From<SourceSequenceId> for Fingerprint {
28    fn from(id: SourceSequenceId) -> Self {
29        id.fingerprint()
30    }
31}
32
33/// Errors produced while decoding a hexadecimal fingerprint.
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35pub enum FingerprintError {
36    #[error("fingerprint must be 64 hexadecimal characters, found {0}")]
37    Length(usize),
38    #[error("fingerprint contains non-hexadecimal character `{0}`")]
39    Digit(char),
40}
41
42#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub struct Fingerprint([u8; 32]);
44
45impl Fingerprint {
46    #[must_use]
47    pub fn of<I, T>(fields: I) -> Self
48    where
49        I: IntoIterator<Item = T>,
50        T: AsRef<[u8]>,
51    {
52        let mut hasher = blake3::Hasher::new();
53        for field in fields {
54            let field = field.as_ref();
55            hasher.update(&u64::try_from(field.len()).unwrap_or(u64::MAX).to_le_bytes());
56            hasher.update(field);
57        }
58        Self(*hasher.finalize().as_bytes())
59    }
60
61    #[must_use]
62    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
63        Self(bytes)
64    }
65
66    #[must_use]
67    pub const fn as_bytes(&self) -> &[u8; 32] {
68        &self.0
69    }
70
71    #[must_use]
72    pub const fn to_u64(self) -> u64 {
73        u64::from_le_bytes([
74            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7],
75        ])
76    }
77
78    #[must_use]
79    pub fn to_hex(self) -> String {
80        let mut hex = String::with_capacity(64);
81        for byte in self.0 {
82            hex.push(char::from(HEX_DIGITS[usize::from(byte >> 4)]));
83            hex.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)]));
84        }
85        hex
86    }
87
88    /// Parses a 64-character hexadecimal fingerprint.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error when the input has the wrong length or contains a
93    /// non-hexadecimal character.
94    pub fn from_hex(text: &str) -> Result<Self, FingerprintError> {
95        let bytes = text.as_bytes();
96        if bytes.len() != 64 {
97            return Err(FingerprintError::Length(bytes.len()));
98        }
99        let mut digest = [0_u8; 32];
100        let (pairs, _) = bytes.as_chunks::<2>();
101        for (slot, pair) in digest.iter_mut().zip(pairs) {
102            *slot = (nibble(pair[0])? << 4) | nibble(pair[1])?;
103        }
104        Ok(Self(digest))
105    }
106}
107
108fn nibble(byte: u8) -> Result<u8, FingerprintError> {
109    match byte {
110        b'0'..=b'9' => Ok(byte - b'0'),
111        b'a'..=b'f' => Ok(byte - b'a' + 10),
112        b'A'..=b'F' => Ok(byte - b'A' + 10),
113        _ => Err(FingerprintError::Digit(char::from(byte))),
114    }
115}
116
117impl fmt::Display for Fingerprint {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.write_str(&self.to_hex())
120    }
121}
122
123impl fmt::Debug for Fingerprint {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "Fingerprint({})", self.to_hex())
126    }
127}
128
129impl Serialize for Fingerprint {
130    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
131        serializer.serialize_str(&self.to_hex())
132    }
133}
134
135impl<'de> Deserialize<'de> for Fingerprint {
136    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
137        Self::from_hex(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn separates_fields_so_concatenations_do_not_collide() {
147        assert_ne!(Fingerprint::of(["ab", "c"]), Fingerprint::of(["a", "bc"]));
148        assert_ne!(Fingerprint::of(["a\0", "b"]), Fingerprint::of(["a", "\0b"]));
149        assert_eq!(Fingerprint::of(["a", "b"]), Fingerprint::of(["a", "b"]));
150    }
151
152    #[test]
153    fn round_trips_through_hexadecimal() {
154        let fingerprint = Fingerprint::of(["value"]);
155        let hex = fingerprint.to_hex();
156        assert_eq!(hex.len(), 64);
157        assert_eq!(Fingerprint::from_hex(&hex).unwrap(), fingerprint);
158        assert!(Fingerprint::from_hex("beef").is_err());
159        assert!(Fingerprint::from_hex(&"z".repeat(64)).is_err());
160    }
161
162    #[test]
163    fn source_sequence_identity_preserves_order_and_line_boundaries() {
164        let empty = SourceSequenceId::from_lines(std::iter::empty());
165        assert_eq!(empty, SourceSequenceId::from_lines(std::iter::empty()));
166        assert_eq!(
167            SourceSequenceId::from_lines(["a", "b"]),
168            SourceSequenceId::from_lines(["a", "b"])
169        );
170        assert_ne!(
171            SourceSequenceId::from_lines(["a", "b"]),
172            SourceSequenceId::from_lines(["b", "a"])
173        );
174        assert_ne!(
175            SourceSequenceId::from_lines(["ab", "c"]),
176            SourceSequenceId::from_lines(["a", "bc"])
177        );
178        assert_eq!(empty.fingerprint(), empty.0);
179    }
180
181    #[test]
182    fn serializes_as_a_hexadecimal_string() {
183        let fingerprint = Fingerprint::of(["value"]);
184        let json = serde_json::to_string(&fingerprint).unwrap();
185        assert_eq!(json, format!("\"{}\"", fingerprint.to_hex()));
186        assert_eq!(
187            serde_json::from_str::<Fingerprint>(&json).unwrap(),
188            fingerprint
189        );
190    }
191}