gitoid/
gitoid.rs

1//! A gitoid representing a single artifact.
2
3use crate::{
4    internal::gitoid_from_buffer, util::stream_len::stream_len, Error, HashAlgorithm, ObjectType,
5    Result,
6};
7use core::{
8    cmp::Ordering,
9    fmt::{Debug, Formatter, Result as FmtResult},
10    hash::{Hash, Hasher},
11    marker::PhantomData,
12};
13use digest::OutputSizeUser;
14
15#[cfg(feature = "async")]
16use {
17    crate::{internal::gitoid_from_async_reader, util::stream_len::async_stream_len},
18    tokio::io::{AsyncRead, AsyncSeek},
19};
20
21#[cfg(feature = "std")]
22use {
23    crate::{gitoid_url_parser::GitOidUrlParser, internal::gitoid_from_reader},
24    serde::{
25        de::{Deserializer, Error as DeserializeError, Visitor},
26        Deserialize, Serialize, Serializer,
27    },
28    std::{
29        fmt::Display,
30        io::{Read, Seek},
31        result::Result as StdResult,
32        str::FromStr,
33    },
34    url::Url,
35};
36
37/// A struct that computes [gitoids][g] based on the selected algorithm
38///
39/// [g]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
40#[repr(C)]
41pub struct GitOid<H, O>
42where
43    H: HashAlgorithm,
44    O: ObjectType,
45{
46    #[doc(hidden)]
47    pub(crate) _phantom: PhantomData<O>,
48
49    #[doc(hidden)]
50    pub(crate) value: H::Array,
51}
52
53#[cfg(feature = "std")]
54pub(crate) const GITOID_URL_SCHEME: &str = "gitoid";
55
56impl<H, O> GitOid<H, O>
57where
58    H: HashAlgorithm,
59    O: ObjectType,
60{
61    /// Create a new `GitOid` based on a slice of bytes.
62    pub fn id_bytes<B: AsRef<[u8]>>(content: B) -> GitOid<H, O> {
63        fn inner<H, O>(content: &[u8]) -> GitOid<H, O>
64        where
65            H: HashAlgorithm,
66            O: ObjectType,
67        {
68            // PANIC SAFETY: We're reading from an in-memory buffer, so no IO errors can arise.
69            gitoid_from_buffer(H::new(), content, content.len()).unwrap()
70        }
71
72        inner(content.as_ref())
73    }
74
75    /// Create a `GitOid` from a UTF-8 string slice.
76    pub fn id_str<S: AsRef<str>>(s: S) -> GitOid<H, O> {
77        fn inner<H, O>(s: &str) -> GitOid<H, O>
78        where
79            H: HashAlgorithm,
80            O: ObjectType,
81        {
82            GitOid::id_bytes(s.as_bytes())
83        }
84
85        inner(s.as_ref())
86    }
87
88    #[cfg(feature = "std")]
89    /// Create a `GitOid` from a reader.
90    pub fn id_reader<R: Read + Seek>(mut reader: R) -> Result<GitOid<H, O>> {
91        let expected_length = stream_len(&mut reader)? as usize;
92        GitOid::id_reader_with_length(reader, expected_length)
93    }
94
95    #[cfg(feature = "std")]
96    /// Generate a `GitOid` from a reader, providing an expected length in bytes.
97    pub fn id_reader_with_length<R>(reader: R, expected_length: usize) -> Result<GitOid<H, O>>
98    where
99        R: Read + Seek,
100    {
101        gitoid_from_reader(H::new(), reader, expected_length)
102    }
103
104    #[cfg(feature = "async")]
105    /// Generate a `GitOid` from an asynchronous reader.
106    pub async fn id_async_reader<R: AsyncRead + AsyncSeek + Unpin>(
107        mut reader: R,
108    ) -> Result<GitOid<H, O>> {
109        let expected_length = async_stream_len(&mut reader).await? as usize;
110        GitOid::id_async_reader_with_length(reader, expected_length).await
111    }
112
113    #[cfg(feature = "async")]
114    /// Generate a `GitOid` from an asynchronous reader, providing an expected length in bytes.
115    pub async fn id_async_reader_with_length<R: AsyncRead + AsyncSeek + Unpin>(
116        reader: R,
117        expected_length: usize,
118    ) -> Result<GitOid<H, O>> {
119        gitoid_from_async_reader(H::new(), reader, expected_length).await
120    }
121
122    #[cfg(feature = "std")]
123    /// Construct a new `GitOid` from a `Url`.
124    pub fn try_from_url(url: Url) -> Result<GitOid<H, O>> {
125        GitOid::try_from(url)
126    }
127
128    #[cfg(feature = "std")]
129    /// Get a URL for the current `GitOid`.
130    pub fn url(&self) -> Url {
131        // PANIC SAFETY: We know that this is a valid URL;
132        //               our `Display` impl is the URL representation.
133        Url::parse(&self.to_string()).unwrap()
134    }
135
136    /// Get the underlying bytes of the hash.
137    pub fn as_bytes(&self) -> &[u8] {
138        &self.value[..]
139    }
140
141    #[cfg(feature = "std")]
142    /// Convert the hash to a hexadecimal string.
143    pub fn as_hex(&self) -> String {
144        hex::encode(self.as_bytes())
145    }
146
147    /// Get the hash algorithm used for the `GitOid`.
148    pub const fn hash_algorithm(&self) -> &'static str {
149        H::NAME
150    }
151
152    /// Get the object type of the `GitOid`.
153    pub const fn object_type(&self) -> &'static str {
154        O::NAME
155    }
156
157    /// Get the length of the hash in bytes.
158    pub fn hash_len(&self) -> usize {
159        <H::Alg as OutputSizeUser>::output_size()
160    }
161}
162
163#[cfg(feature = "std")]
164impl<H, O> FromStr for GitOid<H, O>
165where
166    H: HashAlgorithm,
167    O: ObjectType,
168{
169    type Err = Error;
170
171    fn from_str(s: &str) -> Result<GitOid<H, O>> {
172        let url = Url::parse(s)?;
173        GitOid::try_from_url(url)
174    }
175}
176
177impl<H, O> Clone for GitOid<H, O>
178where
179    H: HashAlgorithm,
180    O: ObjectType,
181{
182    fn clone(&self) -> Self {
183        *self
184    }
185}
186
187impl<H, O> Copy for GitOid<H, O>
188where
189    H: HashAlgorithm,
190    O: ObjectType,
191{
192}
193
194impl<H, O> PartialEq<GitOid<H, O>> for GitOid<H, O>
195where
196    H: HashAlgorithm,
197    O: ObjectType,
198{
199    fn eq(&self, other: &GitOid<H, O>) -> bool {
200        self.value == other.value
201    }
202}
203
204impl<H, O> Eq for GitOid<H, O>
205where
206    H: HashAlgorithm,
207    O: ObjectType,
208{
209}
210
211impl<H, O> PartialOrd<GitOid<H, O>> for GitOid<H, O>
212where
213    H: HashAlgorithm,
214    O: ObjectType,
215{
216    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
217        Some(self.cmp(other))
218    }
219}
220
221impl<H, O> Ord for GitOid<H, O>
222where
223    H: HashAlgorithm,
224    O: ObjectType,
225{
226    fn cmp(&self, other: &Self) -> Ordering {
227        self.value.cmp(&other.value)
228    }
229}
230
231impl<H, O> Hash for GitOid<H, O>
232where
233    H: HashAlgorithm,
234    O: ObjectType,
235{
236    fn hash<H2>(&self, state: &mut H2)
237    where
238        H2: Hasher,
239    {
240        self.value.hash(state);
241    }
242}
243
244impl<H, O> Debug for GitOid<H, O>
245where
246    H: HashAlgorithm,
247    O: ObjectType,
248{
249    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
250        f.debug_struct("GitOid")
251            .field("object_type", &O::NAME)
252            .field("hash_algorithm", &H::NAME)
253            .field("value", &self.value)
254            .finish()
255    }
256}
257
258#[cfg(feature = "std")]
259impl<H, O> Display for GitOid<H, O>
260where
261    H: HashAlgorithm,
262    O: ObjectType,
263{
264    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
265        write!(
266            f,
267            "{}:{}:{}:{}",
268            GITOID_URL_SCHEME,
269            O::NAME,
270            H::NAME,
271            self.as_hex()
272        )
273    }
274}
275
276#[cfg(feature = "std")]
277impl<H, O> Serialize for GitOid<H, O>
278where
279    H: HashAlgorithm,
280    O: ObjectType,
281{
282    fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>
283    where
284        S: Serializer,
285    {
286        // Serialize self as the URL string.
287        let self_as_url_str = self.url().to_string();
288        serializer.serialize_str(&self_as_url_str)
289    }
290}
291
292#[cfg(feature = "std")]
293impl<'de, H, O> Deserialize<'de> for GitOid<H, O>
294where
295    H: HashAlgorithm,
296    O: ObjectType,
297{
298    fn deserialize<D>(deserializer: D) -> StdResult<Self, D::Error>
299    where
300        D: Deserializer<'de>,
301    {
302        // Deserialize self from the URL string.
303        struct GitOidVisitor<H: HashAlgorithm, O: ObjectType>(PhantomData<H>, PhantomData<O>);
304
305        impl<H: HashAlgorithm, O: ObjectType> Visitor<'_> for GitOidVisitor<H, O> {
306            type Value = GitOid<H, O>;
307
308            fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
309                formatter.write_str("a gitoid-scheme URL")
310            }
311
312            fn visit_str<E>(self, value: &str) -> StdResult<Self::Value, E>
313            where
314                E: DeserializeError,
315            {
316                let url = Url::parse(value).map_err(E::custom)?;
317                let id = GitOid::try_from(url).map_err(E::custom)?;
318                Ok(id)
319            }
320        }
321
322        deserializer.deserialize_str(GitOidVisitor(PhantomData, PhantomData))
323    }
324}
325
326#[cfg(feature = "std")]
327impl<H, O> TryFrom<Url> for GitOid<H, O>
328where
329    H: HashAlgorithm,
330    O: ObjectType,
331{
332    type Error = Error;
333
334    fn try_from(url: Url) -> Result<GitOid<H, O>> {
335        GitOidUrlParser::new(&url).parse()
336    }
337}