Skip to main content

pijul_core/pristine/
hash.rs

1use super::{BASE32, Base32};
2pub(crate) const BLAKE3_BYTES: usize = 32;
3#[cfg(feature = "git2")]
4pub(crate) const GIT_SHA1_BYTES: usize = 20;
5#[cfg(feature = "git2")]
6pub(crate) const GIT_SHA2_BYTES: usize = 32;
7
8/// The external hash of changes.
9#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
10pub enum Hash {
11    /// None is the hash of the "null change", which introduced a
12    /// single root vertex at the beginning of the repository.
13    None,
14    Blake3([u8; BLAKE3_BYTES]),
15    #[cfg(feature = "git2")]
16    GitSha1([u8; 20]),
17    #[cfg(feature = "git2")]
18    GitSha2([u8; 32]),
19}
20
21pub enum Hasher {
22    Blake3(blake3::Hasher),
23}
24
25impl Default for Hasher {
26    fn default() -> Self {
27        Hasher::Blake3(blake3::Hasher::new())
28    }
29}
30
31impl Hasher {
32    pub fn update(&mut self, bytes: &[u8]) {
33        match self {
34            Hasher::Blake3(h) => {
35                h.update(bytes);
36            }
37        }
38    }
39    pub fn finish(&self) -> Hash {
40        match self {
41            Hasher::Blake3(h) => {
42                let result = h.finalize();
43                let mut hash = [0; BLAKE3_BYTES];
44                hash.clone_from_slice(result.as_bytes());
45                Hash::Blake3(hash)
46            }
47        }
48    }
49}
50
51impl std::fmt::Debug for Hash {
52    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
53        write!(fmt, "{}", self.to_base32())
54    }
55}
56
57/// Algorithm used to compute change hashes.
58#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
59#[repr(u8)]
60pub enum HashAlgorithm {
61    None = 0,
62    Blake3 = 1,
63    GitSha1 = 2,
64    GitSha2 = 3,
65}
66
67impl Hash {
68    pub fn to_bytes(&self) -> [u8; 1 + BLAKE3_BYTES] {
69        match *self {
70            Hash::None => unimplemented!(),
71            Hash::Blake3(ref s) => {
72                let mut out = [0; 1 + BLAKE3_BYTES];
73                out[0] = HashAlgorithm::Blake3 as u8;
74                out[1..].clone_from_slice(s);
75                out
76            }
77            #[cfg(feature = "git2")]
78            Hash::GitSha1(ref s) => {
79                let mut out = [0; 1 + BLAKE3_BYTES];
80                out[0] = HashAlgorithm::GitSha1 as u8;
81                out[1..21].clone_from_slice(s);
82                out
83            }
84            #[cfg(feature = "git2")]
85            Hash::GitSha2(ref s) => {
86                let mut out = [0; 1 + BLAKE3_BYTES];
87                out[0] = HashAlgorithm::GitSha2 as u8;
88                out[1..].clone_from_slice(s);
89                out
90            }
91        }
92    }
93
94    #[allow(clippy::int_plus_one)]
95    pub fn from_bytes(s: &[u8]) -> Option<Self> {
96        if s.len() >= 1 + BLAKE3_BYTES && s[0] == HashAlgorithm::Blake3 as u8 {
97            let mut out = [0; BLAKE3_BYTES];
98            out.clone_from_slice(&s[1..]);
99            return Some(Hash::Blake3(out));
100        }
101
102        #[cfg(feature = "git2")]
103        if s.len() >= 1 + GIT_SHA1_BYTES && s[0] == HashAlgorithm::GitSha1 as u8 {
104            let mut out = [0; GIT_SHA1_BYTES];
105            out.clone_from_slice(&s[1..]);
106            return Some(Hash::GitSha1(out));
107        }
108
109        #[cfg(feature = "git2")]
110        if s.len() >= 1 + GIT_SHA2_BYTES && s[0] == HashAlgorithm::GitSha2 as u8 {
111            let mut out = [0; GIT_SHA2_BYTES];
112            out.clone_from_slice(&s[1..]);
113            return Some(Hash::GitSha2(out));
114        }
115
116        None
117    }
118}
119
120#[cfg(feature = "git2")]
121impl TryInto<git2::Oid> for Hash {
122    type Error = ();
123    fn try_into(self) -> Result<git2::Oid, Self::Error> {
124        match self {
125            Hash::GitSha1(s) => Ok(git2::Oid::from_bytes(&s).unwrap()),
126            Hash::GitSha2(s) => Ok(git2::Oid::from_bytes(&s).unwrap()),
127            _ => Err(()),
128        }
129    }
130}
131
132pub fn prefix_guesses(s: &str) -> impl Iterator<Item = Hash> {
133    #[cfg(not(feature = "git2"))]
134    let hashes = [Hash::None, Hash::Blake3([0; BLAKE3_BYTES])];
135
136    #[cfg(feature = "git2")]
137    let hashes = [
138        Hash::None,
139        Hash::Blake3([0; BLAKE3_BYTES]),
140        Hash::GitSha1([0; GIT_SHA1_BYTES]),
141        Hash::GitSha2([0; GIT_SHA2_BYTES]),
142    ];
143
144    let from = Hash::from_base32(s.as_bytes());
145    let from_is_some = from.is_some();
146    let decode_len = (s.len() * 5) / 8;
147    let s = s.split_at(BASE32.encode_len(decode_len)).0;
148    debug!("decode_len {:?} {:?}", decode_len, from);
149    from.into_iter()
150        .chain(hashes.into_iter().filter_map(move |h| match h {
151            _ if from_is_some => None,
152            Hash::None if s == "A" || s == "AA" => Some(Hash::None),
153            Hash::Blake3(mut b) if b.len() >= decode_len => {
154                match BASE32.decode_mut(s.as_bytes(), &mut b[..decode_len]) {
155                    Err(data_encoding::DecodePartial {
156                        error:
157                            data_encoding::DecodeError {
158                                kind: data_encoding::DecodeKind::Symbol,
159                                ..
160                            },
161                        ..
162                    }) => None,
163                    _ => Some(Hash::Blake3(b)),
164                }
165            }
166            #[cfg(feature = "git2")]
167            Hash::GitSha1(mut b) if b.len() >= decode_len => {
168                match BASE32.decode_mut(s.as_bytes(), &mut b[..decode_len]) {
169                    Err(data_encoding::DecodePartial {
170                        error:
171                            data_encoding::DecodeError {
172                                kind: data_encoding::DecodeKind::Symbol,
173                                ..
174                            },
175                        ..
176                    }) => None,
177                    _ => Some(Hash::GitSha1(b)),
178                }
179            }
180            #[cfg(feature = "git2")]
181            Hash::GitSha2(mut b) if b.len() >= decode_len => {
182                match BASE32.decode_mut(s.as_bytes(), &mut b[..decode_len]) {
183                    Err(data_encoding::DecodePartial {
184                        error:
185                            data_encoding::DecodeError {
186                                kind: data_encoding::DecodeKind::Symbol,
187                                ..
188                            },
189                        ..
190                    }) => None,
191                    _ => Some(Hash::GitSha2(b)),
192                }
193            }
194            x => {
195                debug!("none {:?}", x);
196                None
197            }
198        }))
199}
200
201impl super::Base32 for Hash {
202    /// Returns the base-32 representation of a hash.
203    fn to_base32(&self) -> String {
204        match *self {
205            Hash::None => BASE32.encode(&[0]),
206            Hash::Blake3(ref s) => {
207                let mut hash = [0; 1 + BLAKE3_BYTES];
208                hash[BLAKE3_BYTES] = HashAlgorithm::Blake3 as u8;
209                hash[..BLAKE3_BYTES].clone_from_slice(s);
210                BASE32.encode(&hash)
211            }
212            #[cfg(feature = "git2")]
213            Hash::GitSha1(ref s) => {
214                let mut hash = [0; 1 + GIT_SHA1_BYTES];
215                hash[20] = HashAlgorithm::GitSha1 as u8;
216                hash[..GIT_SHA1_BYTES].clone_from_slice(s);
217                BASE32.encode(&hash)
218            }
219            #[cfg(feature = "git2")]
220            Hash::GitSha2(ref s) => {
221                let mut hash = [0; 1 + GIT_SHA2_BYTES];
222                hash[20] = HashAlgorithm::GitSha2 as u8;
223                hash[..GIT_SHA2_BYTES].clone_from_slice(s);
224                BASE32.encode(&hash)
225            }
226        }
227    }
228
229    /// Parses a base-32 string into a hash.
230    fn from_base32(s: &[u8]) -> Option<Self> {
231        let bytes = if let Ok(s) = BASE32.decode(s) {
232            s
233        } else {
234            return None;
235        };
236        if bytes == [0] {
237            return Some(Hash::None);
238        } else if bytes.len() == BLAKE3_BYTES + 1
239            && bytes[BLAKE3_BYTES] == HashAlgorithm::Blake3 as u8
240        {
241            let mut hash = [0; BLAKE3_BYTES];
242            hash.clone_from_slice(&bytes[..BLAKE3_BYTES]);
243            return Some(Hash::Blake3(hash));
244        }
245
246        #[cfg(feature = "git2")]
247        if bytes.len() == GIT_SHA1_BYTES + 1
248            && bytes[GIT_SHA1_BYTES] == HashAlgorithm::GitSha1 as u8
249        {
250            let mut hash = [0; GIT_SHA1_BYTES];
251            hash.clone_from_slice(&bytes[..GIT_SHA1_BYTES]);
252            return Some(Hash::GitSha1(hash));
253        }
254
255        #[cfg(feature = "git2")]
256        if bytes.len() == GIT_SHA2_BYTES + 1
257            && bytes[GIT_SHA2_BYTES] == HashAlgorithm::GitSha2 as u8
258        {
259            let mut hash = [0; GIT_SHA2_BYTES];
260            hash.clone_from_slice(&bytes[..GIT_SHA2_BYTES]);
261            return Some(Hash::GitSha2(hash));
262        }
263
264        None
265    }
266}
267
268impl std::str::FromStr for Hash {
269    type Err = crate::ParseError;
270    fn from_str(s: &str) -> Result<Self, Self::Err> {
271        if let Some(b) = Self::from_base32(s.as_bytes()) {
272            Ok(b)
273        } else {
274            Err(crate::ParseError { s: s.to_string() })
275        }
276    }
277}
278
279#[test]
280fn from_to() {
281    let mut h = Hasher::default();
282    h.update(b"blabla");
283    let h = h.finish();
284    assert_eq!(Hash::from_base32(&h.to_base32().as_bytes()), Some(h));
285    let h = Hash::None;
286    assert_eq!(Hash::from_base32(&h.to_base32().as_bytes()), Some(h));
287    let b = BASE32.encode(&[19, 18, 17]);
288    assert_eq!(Hash::from_base32(&b.as_bytes()), None);
289}
290
291#[derive(Clone, Copy)]
292#[repr(C)]
293pub struct SerializedHash {
294    pub(crate) t: u8,
295    h: H,
296}
297
298impl std::hash::Hash for SerializedHash {
299    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
300        self.t.hash(hasher);
301        if self.t == HashAlgorithm::Blake3 as u8 {
302            unsafe { self.h.blake3.hash(hasher) }
303        }
304
305        #[cfg(feature = "git2")]
306        if self.t == HashAlgorithm::GitSha1 as u8 {
307            unsafe { self.h.git_sha1.hash(hasher) }
308        }
309
310        #[cfg(feature = "git2")]
311        if self.t == HashAlgorithm::GitSha2 as u8 {
312            unsafe { self.h.git_sha2.hash(hasher) }
313        }
314    }
315}
316
317#[derive(Clone, Copy)]
318pub(crate) union H {
319    none: (),
320    blake3: [u8; BLAKE3_BYTES],
321    #[cfg(feature = "git2")]
322    git_sha1: [u8; GIT_SHA1_BYTES],
323    #[cfg(feature = "git2")]
324    git_sha2: [u8; GIT_SHA2_BYTES],
325}
326
327pub(crate) const HASH_NONE: SerializedHash = SerializedHash {
328    t: HashAlgorithm::None as u8,
329    h: H { none: () },
330};
331
332use std::cmp::Ordering;
333
334impl PartialOrd for SerializedHash {
335    fn partial_cmp(&self, b: &Self) -> Option<Ordering> {
336        Some(self.cmp(b))
337    }
338}
339
340impl Ord for SerializedHash {
341    fn cmp(&self, b: &Self) -> Ordering {
342        match self.t.cmp(&b.t) {
343            Ordering::Equal => {
344                if self.t == HashAlgorithm::Blake3 as u8 {
345                    return unsafe { self.h.blake3.cmp(&b.h.blake3) };
346                }
347
348                #[cfg(feature = "git2")]
349                if self.t == HashAlgorithm::GitSha1 as u8 {
350                    return unsafe { self.h.git_sha1.cmp(&b.h.git_sha1) };
351                } else if self.t == HashAlgorithm::GitSha2 as u8 {
352                    return unsafe { self.h.git_sha2.cmp(&b.h.git_sha2) };
353                }
354
355                Ordering::Equal
356            }
357            o => o,
358        }
359    }
360}
361
362impl PartialEq for SerializedHash {
363    fn eq(&self, b: &Self) -> bool {
364        if self.t == HashAlgorithm::Blake3 as u8 && self.t == b.t {
365            return unsafe { self.h.blake3 == b.h.blake3 };
366        }
367
368        #[cfg(feature = "git2")]
369        if self.t == HashAlgorithm::GitSha1 as u8 && self.t == b.t {
370            return unsafe { self.h.git_sha1 == b.h.git_sha1 };
371        } else if self.t == HashAlgorithm::GitSha2 as u8 && self.t == b.t {
372            return unsafe { self.h.git_sha2 == b.h.git_sha2 };
373        }
374
375        self.t == b.t
376    }
377}
378impl Eq for SerializedHash {}
379
380impl std::fmt::Debug for SerializedHash {
381    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
382        Hash::from(self).fmt(fmt)
383    }
384}
385
386impl<'a> From<&'a SerializedHash> for Hash {
387    fn from(s: &'a SerializedHash) -> Hash {
388        if s.t == HashAlgorithm::Blake3 as u8 {
389            return Hash::Blake3(unsafe { s.h.blake3 });
390        }
391
392        #[cfg(feature = "git2")]
393        if s.t == HashAlgorithm::GitSha1 as u8 {
394            return Hash::GitSha1(unsafe { s.h.git_sha1 });
395        } else if s.t == HashAlgorithm::GitSha2 as u8 {
396            return Hash::GitSha2(unsafe { s.h.git_sha2 });
397        }
398
399        if s.t == HashAlgorithm::None as u8 {
400            Hash::None
401        } else {
402            panic!("Unknown hash algorithm {:?}", s.t)
403        }
404    }
405}
406
407#[cfg(feature = "git2")]
408impl From<git2::Oid> for Hash {
409    fn from(s: git2::Oid) -> Hash {
410        let b = s.as_bytes();
411        if b.len() == 20 {
412            let mut h = [0; GIT_SHA1_BYTES];
413            h.clone_from_slice(b);
414            Hash::GitSha1(h)
415        } else {
416            assert_eq!(b.len(), GIT_SHA2_BYTES);
417            let mut h = [0; GIT_SHA2_BYTES];
418            h.clone_from_slice(b);
419            Hash::GitSha2(h)
420        }
421    }
422}
423
424impl From<SerializedHash> for Hash {
425    fn from(s: SerializedHash) -> Hash {
426        (&s).into()
427    }
428}
429
430impl From<Hash> for SerializedHash {
431    fn from(s: Hash) -> SerializedHash {
432        (&s).into()
433    }
434}
435
436impl<'a> From<&'a Hash> for SerializedHash {
437    fn from(s: &'a Hash) -> Self {
438        match s {
439            Hash::Blake3(s) => SerializedHash {
440                t: HashAlgorithm::Blake3 as u8,
441                h: H { blake3: *s },
442            },
443            #[cfg(feature = "git2")]
444            Hash::GitSha1(s) => SerializedHash {
445                t: HashAlgorithm::GitSha1 as u8,
446                h: H { git_sha1: *s },
447            },
448            #[cfg(feature = "git2")]
449            Hash::GitSha2(s) => SerializedHash {
450                t: HashAlgorithm::GitSha2 as u8,
451                h: H { git_sha2: *s },
452            },
453            Hash::None => SerializedHash {
454                t: 0,
455                h: H { none: () },
456            },
457        }
458    }
459}
460
461impl SerializedHash {
462    pub fn size(b: &[u8]) -> usize {
463        if b[0] == HashAlgorithm::Blake3 as u8 {
464            return 1 + BLAKE3_BYTES;
465        }
466
467        #[cfg(feature = "git2")]
468        if b[0] == HashAlgorithm::GitSha1 as u8 {
469            return 1 + GIT_SHA1_BYTES;
470        }
471
472        #[cfg(feature = "git2")]
473        if b[0] == HashAlgorithm::GitSha2 as u8 {
474            return 1 + GIT_SHA2_BYTES;
475        }
476
477        if b[0] == HashAlgorithm::None as u8 {
478            1
479        } else {
480            panic!("Unknown hash algorithm {:?}", b[0])
481        }
482    }
483}