gix_hash/
object_id.rs

1use std::{
2    borrow::Borrow,
3    hash::{Hash, Hasher},
4    ops::Deref,
5};
6
7use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST};
8
9/// An owned hash identifying objects, most commonly `Sha1`
10#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum ObjectId {
13    /// A SHA 1 hash digest
14    Sha1([u8; SIZE_OF_SHA1_DIGEST]),
15}
16
17// False positive: https://github.com/rust-lang/rust-clippy/issues/2627
18// ignoring some fields while hashing is perfectly valid and just leads to
19// increased HashCollisions. One Sha1 being a prefix of another Sha256 is
20// extremely unlikely to begin with so it doesn't matter.
21// This implementation matches the `Hash` implementation for `oid`
22// and allows the usage of custom Hashers that only copy a truncated ShaHash
23#[allow(clippy::derived_hash_with_manual_eq)]
24impl Hash for ObjectId {
25    fn hash<H: Hasher>(&self, state: &mut H) {
26        state.write(self.as_slice());
27    }
28}
29
30#[allow(missing_docs)]
31pub mod decode {
32    use std::str::FromStr;
33
34    use crate::object_id::ObjectId;
35
36    /// An error returned by [`ObjectId::from_hex()`][crate::ObjectId::from_hex()]
37    #[derive(Debug, thiserror::Error)]
38    #[allow(missing_docs)]
39    pub enum Error {
40        #[error("A hash sized {0} hexadecimal characters is invalid")]
41        InvalidHexEncodingLength(usize),
42        #[error("Invalid character encountered")]
43        Invalid,
44    }
45
46    /// Hash decoding
47    impl ObjectId {
48        /// Create an instance from a `buffer` of 40 bytes encoded with hexadecimal notation.
49        ///
50        /// Such a buffer can be obtained using [`oid::write_hex_to(buffer)`][super::oid::write_hex_to()]
51        pub fn from_hex(buffer: &[u8]) -> Result<ObjectId, Error> {
52            match buffer.len() {
53                40 => Ok({
54                    ObjectId::Sha1({
55                        let mut buf = [0; 20];
56                        faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err {
57                            faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => Error::Invalid,
58                            faster_hex::Error::InvalidLength(_) => {
59                                unreachable!("BUG: This is already checked")
60                            }
61                        })?;
62                        buf
63                    })
64                }),
65                len => Err(Error::InvalidHexEncodingLength(len)),
66            }
67        }
68    }
69
70    impl FromStr for ObjectId {
71        type Err = Error;
72
73        fn from_str(s: &str) -> Result<Self, Self::Err> {
74            Self::from_hex(s.as_bytes())
75        }
76    }
77}
78
79/// Access and conversion
80impl ObjectId {
81    /// Returns the kind of hash used in this instance.
82    #[inline]
83    pub fn kind(&self) -> Kind {
84        match self {
85            ObjectId::Sha1(_) => Kind::Sha1,
86        }
87    }
88    /// Return the raw byte slice representing this hash.
89    #[inline]
90    pub fn as_slice(&self) -> &[u8] {
91        match self {
92            Self::Sha1(b) => b.as_ref(),
93        }
94    }
95    /// Return the raw mutable byte slice representing this hash.
96    #[inline]
97    pub fn as_mut_slice(&mut self) -> &mut [u8] {
98        match self {
99            Self::Sha1(b) => b.as_mut(),
100        }
101    }
102
103    /// The hash of an empty blob.
104    #[inline]
105    pub const fn empty_blob(hash: Kind) -> ObjectId {
106        match hash {
107            Kind::Sha1 => {
108                ObjectId::Sha1(*b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91")
109            }
110        }
111    }
112
113    /// The hash of an empty tree.
114    #[inline]
115    pub const fn empty_tree(hash: Kind) -> ObjectId {
116        match hash {
117            Kind::Sha1 => {
118                ObjectId::Sha1(*b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04")
119            }
120        }
121    }
122
123    /// Returns an instances whose bytes are all zero.
124    #[inline]
125    #[doc(alias = "zero", alias = "git2")]
126    pub const fn null(kind: Kind) -> ObjectId {
127        match kind {
128            Kind::Sha1 => Self::null_sha1(),
129        }
130    }
131
132    /// Returns `true` if this hash consists of all null bytes.
133    #[inline]
134    #[doc(alias = "is_zero", alias = "git2")]
135    pub fn is_null(&self) -> bool {
136        match self {
137            ObjectId::Sha1(digest) => &digest[..] == oid::null_sha1().as_bytes(),
138        }
139    }
140
141    /// Returns `true` if this hash is equal to an empty blob.
142    #[inline]
143    pub fn is_empty_blob(&self) -> bool {
144        self == &Self::empty_blob(self.kind())
145    }
146
147    /// Returns `true` if this hash is equal to an empty tree.
148    #[inline]
149    pub fn is_empty_tree(&self) -> bool {
150        self == &Self::empty_tree(self.kind())
151    }
152}
153
154/// Lifecycle
155impl ObjectId {
156    /// Convert `bytes` into an owned object Id or panic if the slice length doesn't indicate a supported hash.
157    ///
158    /// Use `Self::try_from(bytes)` for a fallible version.
159    pub fn from_bytes_or_panic(bytes: &[u8]) -> Self {
160        match bytes.len() {
161            20 => Self::Sha1(bytes.try_into().expect("prior length validation")),
162            other => panic!("BUG: unsupported hash len: {other}"),
163        }
164    }
165}
166
167/// Sha1 hash specific methods
168impl ObjectId {
169    /// Instantiate an Digest from 20 bytes of a Sha1 digest.
170    #[inline]
171    fn new_sha1(id: [u8; SIZE_OF_SHA1_DIGEST]) -> Self {
172        ObjectId::Sha1(id)
173    }
174
175    /// Instantiate an Digest from a slice 20 borrowed bytes of a Sha1 digest.
176    ///
177    /// Panics of the slice doesn't have a length of 20.
178    #[inline]
179    pub(crate) fn from_20_bytes(b: &[u8]) -> ObjectId {
180        let mut id = [0; SIZE_OF_SHA1_DIGEST];
181        id.copy_from_slice(b);
182        ObjectId::Sha1(id)
183    }
184
185    /// Returns an Digest representing a Sha1 with whose memory is zeroed.
186    #[inline]
187    pub(crate) const fn null_sha1() -> ObjectId {
188        ObjectId::Sha1([0u8; 20])
189    }
190}
191
192impl std::fmt::Debug for ObjectId {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        match self {
195            ObjectId::Sha1(_hash) => f.write_str("Sha1(")?,
196        }
197        for b in self.as_bytes() {
198            write!(f, "{b:02x}")?;
199        }
200        f.write_str(")")
201    }
202}
203
204impl From<[u8; SIZE_OF_SHA1_DIGEST]> for ObjectId {
205    fn from(v: [u8; 20]) -> Self {
206        Self::new_sha1(v)
207    }
208}
209
210impl From<&oid> for ObjectId {
211    fn from(v: &oid) -> Self {
212        match v.kind() {
213            Kind::Sha1 => ObjectId::from_20_bytes(v.as_bytes()),
214        }
215    }
216}
217
218impl TryFrom<&[u8]> for ObjectId {
219    type Error = crate::Error;
220
221    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
222        Ok(oid::try_from_bytes(bytes)?.into())
223    }
224}
225
226impl Deref for ObjectId {
227    type Target = oid;
228
229    fn deref(&self) -> &Self::Target {
230        self.as_ref()
231    }
232}
233
234impl AsRef<oid> for ObjectId {
235    fn as_ref(&self) -> &oid {
236        oid::from_bytes_unchecked(self.as_slice())
237    }
238}
239
240impl Borrow<oid> for ObjectId {
241    fn borrow(&self) -> &oid {
242        self.as_ref()
243    }
244}
245
246impl std::fmt::Display for ObjectId {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        write!(f, "{}", self.to_hex())
249    }
250}
251
252impl PartialEq<&oid> for ObjectId {
253    fn eq(&self, other: &&oid) -> bool {
254        self.as_ref() == *other
255    }
256}