1use alloc::{boxed::Box, string::String, vec::Vec};
16
17use io_replica::object::ReplicaHash;
18use sha2::{Digest, Sha256};
19
20#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub enum PimdirHashAlgo {
24 #[default]
26 Blake3,
27 Sha256_128,
34}
35
36impl PimdirHashAlgo {
37 pub fn as_str(&self) -> &'static str {
39 match self {
40 Self::Blake3 => "blake3",
41 Self::Sha256_128 => "sha256-128",
42 }
43 }
44
45 pub fn parse(algo: &str) -> Option<Self> {
49 match algo {
50 "blake3" => Some(Self::Blake3),
51 "sha256-128" => Some(Self::Sha256_128),
52 _ => None,
53 }
54 }
55
56 pub fn hash(&self, bytes: &[u8]) -> ReplicaHash {
58 let mut hasher = self.hasher();
59 hasher.update(bytes);
60 hasher.finish()
61 }
62
63 pub fn hasher(&self) -> PimdirHasher {
67 match self {
68 Self::Blake3 => PimdirHasher::Blake3(Box::new(blake3::Hasher::new())),
69 Self::Sha256_128 => PimdirHasher::Sha256_128(Sha256::new()),
70 }
71 }
72}
73
74pub enum PimdirHasher {
77 Blake3(Box<blake3::Hasher>),
81 Sha256_128(Sha256),
83}
84
85impl PimdirHasher {
86 pub fn update(&mut self, bytes: &[u8]) {
88 match self {
89 Self::Blake3(hasher) => {
90 hasher.update(bytes);
91 }
92 Self::Sha256_128(hasher) => hasher.update(bytes),
93 }
94 }
95
96 pub fn finish(self) -> ReplicaHash {
98 let digest: Vec<u8> = match self {
99 Self::Blake3(hasher) => hasher.finalize().as_bytes().to_vec(),
100 Self::Sha256_128(hasher) => hasher.finalize()[..16].to_vec(),
101 };
102
103 ReplicaHash(base32(&digest))
104 }
105}
106
107fn base32(digest: &[u8]) -> String {
114 const ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
115
116 let mut name = String::with_capacity(digest.len().div_ceil(5) * 8);
117 let mut buffer: u16 = 0;
118 let mut bits = 0;
119
120 for byte in digest {
121 buffer = (buffer << 8) | u16::from(*byte);
122 bits += 8;
123 while bits >= 5 {
124 bits -= 5;
125 name.push(ALPHABET[usize::from((buffer >> bits) & 0x1f)] as char);
126 }
127 }
128 if bits > 0 {
129 name.push(ALPHABET[usize::from((buffer << (5 - bits)) & 0x1f)] as char);
130 }
131
132 name
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn sha256_128_matches_the_shape_every_implementation_must_agree_on() {
141 let hash = PimdirHashAlgo::Sha256_128.hash(b"pimdir");
145 assert_eq!(hash.0.len(), 26);
146 assert!(hash.0.chars().all(|c| ALPHABET_CHARS.contains(c)));
147 }
148
149 const ALPHABET_CHARS: &str = "abcdefghijklmnopqrstuvwxyz234567";
150
151 #[test]
152 fn base32_encodes_rfc_4648_vectors_lowercased() {
153 assert_eq!(base32(b"f"), "my");
155 assert_eq!(base32(b"fo"), "mzxq");
156 assert_eq!(base32(b"foo"), "mzxw6");
157 assert_eq!(base32(b"foob"), "mzxw6yq");
158 assert_eq!(base32(b"fooba"), "mzxw6ytb");
159 assert_eq!(base32(b"foobar"), "mzxw6ytboi");
160 }
161
162 #[test]
163 fn a_streamed_body_hashes_like_a_whole_one() {
164 for algo in [PimdirHashAlgo::Blake3, PimdirHashAlgo::Sha256_128] {
165 let mut hasher = algo.hasher();
166 hasher.update(b"BEGIN:VCARD\r\n");
167 hasher.update(b"UID:x\r\nEND:VCARD\r\n");
168 assert_eq!(
169 hasher.finish(),
170 algo.hash(b"BEGIN:VCARD\r\nUID:x\r\nEND:VCARD\r\n")
171 );
172 }
173 }
174
175 #[test]
176 fn the_algorithms_round_trip_through_their_stored_spelling() {
177 for algo in [PimdirHashAlgo::Blake3, PimdirHashAlgo::Sha256_128] {
178 assert_eq!(PimdirHashAlgo::parse(algo.as_str()), Some(algo));
179 }
180 assert_eq!(PimdirHashAlgo::parse("md5"), None);
181 assert_eq!(PimdirHashAlgo::default(), PimdirHashAlgo::Blake3);
182 }
183}