gix_hash/
object_id.rs

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