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