Skip to main content

gix_hash/
object_id.rs

1use std::{
2    borrow::Borrow,
3    hash::{Hash, Hasher},
4    ops::Deref,
5};
6
7#[cfg(feature = "bstr")]
8use bstr::{BStr, BString, ByteSlice};
9
10use crate::{Kind, borrowed::oid};
11
12#[cfg(feature = "sha1")]
13use crate::{EMPTY_BLOB_SHA1, EMPTY_TREE_SHA1, SIZE_OF_SHA1_DIGEST};
14
15#[cfg(feature = "sha256")]
16use crate::{EMPTY_BLOB_SHA256, EMPTY_TREE_SHA256, SIZE_OF_SHA256_DIGEST};
17
18/// An owned hash identifying objects, most commonly `Sha1`
19#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[non_exhaustive]
22pub enum ObjectId {
23    /// A SHA1 hash digest
24    #[cfg(feature = "sha1")]
25    Sha1([u8; SIZE_OF_SHA1_DIGEST]),
26    /// A SHA256 hash digest
27    #[cfg(feature = "sha256")]
28    Sha256([u8; SIZE_OF_SHA256_DIGEST]),
29}
30
31// False positive: https://github.com/rust-lang/rust-clippy/issues/2627
32// ignoring some fields while hashing is perfectly valid and just leads to
33// increased HashCollisions. One SHA1 being a prefix of another SHA256 is
34// extremely unlikely to begin with so it doesn't matter.
35// This implementation matches the `Hash` implementation for `oid`
36// and allows the usage of custom Hashers that only copy a truncated ShaHash
37impl Hash for ObjectId {
38    fn hash<H: Hasher>(&self, state: &mut H) {
39        state.write(self.as_slice());
40    }
41}
42
43#[expect(missing_docs)]
44pub mod decode {
45    use std::str::FromStr;
46
47    use crate::object_id::ObjectId;
48
49    #[cfg(feature = "sha1")]
50    use crate::{SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA1_HEX_DIGEST};
51
52    #[cfg(feature = "sha256")]
53    use crate::{SIZE_OF_SHA256_DIGEST, SIZE_OF_SHA256_HEX_DIGEST};
54
55    /// An error returned by [`ObjectId::from_hex()`][crate::ObjectId::from_hex()]
56    #[derive(Debug, thiserror::Error)]
57    #[expect(missing_docs)]
58    pub enum Error {
59        #[error("A hash sized {0} hexadecimal characters is invalid")]
60        InvalidHexEncodingLength(usize),
61        #[error("Invalid character encountered")]
62        Invalid,
63    }
64
65    /// Hash decoding
66    impl ObjectId {
67        /// Create an instance from a `buffer` of 40 bytes or 64 bytes encoded with hexadecimal
68        /// notation. The former will be interpreted as SHA1 while the latter will be interpreted
69        /// as SHA256 when it is enabled.
70        ///
71        /// Such a buffer can be obtained using [`oid::write_hex_to(buffer)`][super::oid::write_hex_to()]
72        pub fn from_hex(buffer: &[u8]) -> Result<ObjectId, Error> {
73            match buffer.len() {
74                #[cfg(feature = "sha1")]
75                SIZE_OF_SHA1_HEX_DIGEST => Ok({
76                    ObjectId::Sha1({
77                        let mut buf = [0; SIZE_OF_SHA1_DIGEST];
78                        faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err {
79                            faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => Error::Invalid,
80                            faster_hex::Error::InvalidLength(_) => {
81                                unreachable!("BUG: This is already checked")
82                            }
83                        })?;
84                        buf
85                    })
86                }),
87                #[cfg(feature = "sha256")]
88                SIZE_OF_SHA256_HEX_DIGEST => Ok({
89                    ObjectId::Sha256({
90                        let mut buf = [0; SIZE_OF_SHA256_DIGEST];
91                        faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err {
92                            faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => Error::Invalid,
93                            faster_hex::Error::InvalidLength(_) => {
94                                unreachable!("BUG: This is already checked")
95                            }
96                        })?;
97                        buf
98                    })
99                }),
100                len => Err(Error::InvalidHexEncodingLength(len)),
101            }
102        }
103    }
104
105    impl FromStr for ObjectId {
106        type Err = Error;
107
108        fn from_str(s: &str) -> Result<Self, Self::Err> {
109            Self::from_hex(s.as_bytes())
110        }
111    }
112}
113
114/// Access and conversion
115impl ObjectId {
116    /// Returns the kind of hash used in this instance.
117    #[inline]
118    pub fn kind(&self) -> Kind {
119        match self {
120            #[cfg(feature = "sha1")]
121            ObjectId::Sha1(_) => Kind::Sha1,
122            #[cfg(feature = "sha256")]
123            ObjectId::Sha256(_) => Kind::Sha256,
124        }
125    }
126    /// Return the raw byte slice representing this hash.
127    #[inline]
128    pub fn as_slice(&self) -> &[u8] {
129        match self {
130            #[cfg(feature = "sha1")]
131            Self::Sha1(b) => b.as_ref(),
132            #[cfg(feature = "sha256")]
133            Self::Sha256(b) => b.as_ref(),
134        }
135    }
136    /// Return the raw mutable byte slice representing this hash.
137    #[inline]
138    pub fn as_mut_slice(&mut self) -> &mut [u8] {
139        match self {
140            #[cfg(feature = "sha1")]
141            Self::Sha1(b) => b.as_mut(),
142            #[cfg(feature = "sha256")]
143            Self::Sha256(b) => b.as_mut(),
144        }
145    }
146
147    /// The hash of an empty blob.
148    #[inline]
149    pub const fn empty_blob(hash: Kind) -> ObjectId {
150        match hash {
151            #[cfg(feature = "sha1")]
152            Kind::Sha1 => ObjectId::Sha1(*EMPTY_BLOB_SHA1),
153            #[cfg(feature = "sha256")]
154            Kind::Sha256 => ObjectId::Sha256(*EMPTY_BLOB_SHA256),
155        }
156    }
157
158    /// The hash of an empty tree.
159    #[inline]
160    pub const fn empty_tree(hash: Kind) -> ObjectId {
161        match hash {
162            #[cfg(feature = "sha1")]
163            Kind::Sha1 => ObjectId::Sha1(*EMPTY_TREE_SHA1),
164            #[cfg(feature = "sha256")]
165            Kind::Sha256 => ObjectId::Sha256(*EMPTY_TREE_SHA256),
166        }
167    }
168
169    /// Returns an instances whose bytes are all zero.
170    #[inline]
171    #[doc(alias = "zero", alias = "git2")]
172    pub const fn null(kind: Kind) -> ObjectId {
173        match kind {
174            #[cfg(feature = "sha1")]
175            Kind::Sha1 => Self::null_sha1(),
176            #[cfg(feature = "sha256")]
177            Kind::Sha256 => Self::null_sha256(),
178        }
179    }
180
181    /// Returns `true` if this hash consists of all null bytes.
182    #[inline]
183    #[doc(alias = "is_zero", alias = "git2")]
184    pub fn is_null(&self) -> bool {
185        match self {
186            #[cfg(feature = "sha1")]
187            ObjectId::Sha1(digest) => &digest[..] == oid::null_sha1().as_bytes(),
188            #[cfg(feature = "sha256")]
189            ObjectId::Sha256(digest) => &digest[..] == oid::null_sha256().as_bytes(),
190        }
191    }
192
193    /// Returns `true` if this hash is equal to an empty blob.
194    #[inline]
195    pub fn is_empty_blob(&self) -> bool {
196        self == &Self::empty_blob(self.kind())
197    }
198
199    /// Returns `true` if this hash is equal to an empty tree.
200    #[inline]
201    pub fn is_empty_tree(&self) -> bool {
202        self == &Self::empty_tree(self.kind())
203    }
204}
205
206/// Lifecycle
207impl ObjectId {
208    /// Convert `bytes` into an owned object Id or panic if the slice length doesn't indicate a supported hash.
209    ///
210    /// Use `Self::try_from(bytes)` for a fallible version.
211    pub fn from_bytes_or_panic(bytes: &[u8]) -> Self {
212        match bytes.len() {
213            #[cfg(feature = "sha1")]
214            SIZE_OF_SHA1_DIGEST => Self::Sha1(bytes.try_into().expect("prior length validation")),
215            #[cfg(feature = "sha256")]
216            SIZE_OF_SHA256_DIGEST => Self::Sha256(bytes.try_into().expect("prior length validation")),
217            other => panic!("BUG: unsupported hash len: {other}"),
218        }
219    }
220}
221
222/// Methods related to SHA1 and SHA256
223impl ObjectId {
224    /// Instantiate an `ObjectId` from a 20 bytes SHA1 digest.
225    #[inline]
226    #[cfg(feature = "sha1")]
227    fn new_sha1(id: [u8; SIZE_OF_SHA1_DIGEST]) -> Self {
228        ObjectId::Sha1(id)
229    }
230
231    /// Instantiate an `ObjectId` from a 32 bytes SHA256 digest.
232    #[inline]
233    #[cfg(feature = "sha256")]
234    fn new_sha256(id: [u8; SIZE_OF_SHA256_DIGEST]) -> Self {
235        ObjectId::Sha256(id)
236    }
237
238    /// Instantiate an `ObjectId` from a borrowed 20 bytes SHA1 digest.
239    ///
240    /// Panics if the slice doesn't have a length of 20.
241    #[inline]
242    #[cfg(feature = "sha1")]
243    pub(crate) fn from_20_bytes(b: &[u8]) -> ObjectId {
244        let mut id = [0; SIZE_OF_SHA1_DIGEST];
245        id.copy_from_slice(b);
246        ObjectId::Sha1(id)
247    }
248
249    /// Instantiate an `ObjectId` from a borrowed 32 bytes SHA256 digest.
250    ///
251    /// Panics if the slice doesn't have a length of 32.
252    #[inline]
253    #[cfg(feature = "sha256")]
254    pub(crate) fn from_32_bytes(b: &[u8]) -> ObjectId {
255        let mut id = [0; SIZE_OF_SHA256_DIGEST];
256        id.copy_from_slice(b);
257        ObjectId::Sha256(id)
258    }
259
260    /// Returns an `ObjectId` representing a SHA1 whose memory is zeroed.
261    #[inline]
262    #[cfg(feature = "sha1")]
263    pub(crate) const fn null_sha1() -> ObjectId {
264        ObjectId::Sha1([0u8; SIZE_OF_SHA1_DIGEST])
265    }
266
267    /// Returns an `ObjectId` representing a SHA256 whose memory is zeroed.
268    #[inline]
269    #[cfg(feature = "sha256")]
270    pub(crate) const fn null_sha256() -> ObjectId {
271        ObjectId::Sha256([0u8; SIZE_OF_SHA256_DIGEST])
272    }
273}
274
275impl std::fmt::Debug for ObjectId {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        match self {
278            #[cfg(feature = "sha1")]
279            ObjectId::Sha1(_hash) => f.write_str("Sha1(")?,
280            #[cfg(feature = "sha256")]
281            ObjectId::Sha256(_) => f.write_str("Sha256(")?,
282        }
283        for b in self.as_bytes() {
284            write!(f, "{b:02x}")?;
285        }
286        f.write_str(")")
287    }
288}
289
290#[cfg(feature = "sha1")]
291impl From<[u8; SIZE_OF_SHA1_DIGEST]> for ObjectId {
292    fn from(v: [u8; SIZE_OF_SHA1_DIGEST]) -> Self {
293        Self::new_sha1(v)
294    }
295}
296
297#[cfg(feature = "sha256")]
298impl From<[u8; SIZE_OF_SHA256_DIGEST]> for ObjectId {
299    fn from(v: [u8; SIZE_OF_SHA256_DIGEST]) -> Self {
300        Self::new_sha256(v)
301    }
302}
303
304impl From<&oid> for ObjectId {
305    fn from(v: &oid) -> Self {
306        match v.kind() {
307            #[cfg(feature = "sha1")]
308            Kind::Sha1 => ObjectId::from_20_bytes(v.as_bytes()),
309            #[cfg(feature = "sha256")]
310            Kind::Sha256 => ObjectId::from_32_bytes(v.as_bytes()),
311        }
312    }
313}
314
315impl TryFrom<&[u8]> for ObjectId {
316    type Error = crate::Error;
317
318    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
319        Ok(oid::try_from_bytes(bytes)?.into())
320    }
321}
322
323impl Deref for ObjectId {
324    type Target = oid;
325
326    fn deref(&self) -> &Self::Target {
327        self.as_ref()
328    }
329}
330
331impl AsRef<oid> for ObjectId {
332    fn as_ref(&self) -> &oid {
333        oid::from_bytes_unchecked(self.as_slice())
334    }
335}
336
337impl Borrow<oid> for ObjectId {
338    fn borrow(&self) -> &oid {
339        self.as_ref()
340    }
341}
342
343impl std::fmt::Display for ObjectId {
344    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        write!(f, "{}", self.to_hex())
346    }
347}
348
349impl ObjectId {
350    fn eq_str(&self, other: &str) -> bool {
351        self.as_ref().eq_str(other)
352    }
353
354    #[cfg(feature = "bstr")]
355    fn eq_bstr(&self, other: &BStr) -> bool {
356        let mut hex = Kind::hex_buf();
357        self.as_ref().hex_to_buf(&mut hex).as_bytes() == other.as_bytes()
358    }
359}
360
361impl_partial_eq_str!(ObjectId);
362
363#[cfg(feature = "bstr")]
364impl PartialEq<BStr> for ObjectId {
365    fn eq(&self, other: &BStr) -> bool {
366        self.eq_bstr(other)
367    }
368}
369
370#[cfg(feature = "bstr")]
371impl PartialEq<&BStr> for ObjectId {
372    fn eq(&self, other: &&BStr) -> bool {
373        self.eq_bstr(other)
374    }
375}
376
377#[cfg(feature = "bstr")]
378impl PartialEq<BString> for ObjectId {
379    fn eq(&self, other: &BString) -> bool {
380        self.eq_bstr(other.as_bstr())
381    }
382}
383
384#[cfg(feature = "bstr")]
385impl PartialEq<ObjectId> for BStr {
386    fn eq(&self, other: &ObjectId) -> bool {
387        other.eq_bstr(self)
388    }
389}
390
391#[cfg(feature = "bstr")]
392impl PartialEq<ObjectId> for &BStr {
393    fn eq(&self, other: &ObjectId) -> bool {
394        other.eq_bstr(self)
395    }
396}
397
398#[cfg(feature = "bstr")]
399impl PartialEq<ObjectId> for BString {
400    fn eq(&self, other: &ObjectId) -> bool {
401        other.eq_bstr(self.as_bstr())
402    }
403}
404
405impl PartialEq<&oid> for ObjectId {
406    fn eq(&self, other: &&oid) -> bool {
407        self.as_ref() == *other
408    }
409}