Skip to main content

gix_hash/
object_id.rs

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