Skip to main content

gix_object/commit/
ref_iter.rs

1use std::{borrow::Cow, ops::Range};
2
3use bstr::BStr;
4use gix_hash::{ObjectId, oid};
5
6use crate::{
7    CommitRefIter,
8    bstr::ByteSlice,
9    commit::{decode, signature_field_name},
10    parse,
11    signature::SignedData,
12};
13
14#[derive(Copy, Clone)]
15pub(crate) enum SignatureKind {
16    Author,
17    Committer,
18}
19
20#[derive(Default, Copy, Clone)]
21pub(crate) enum State {
22    #[default]
23    Tree,
24    Parents,
25    Signature {
26        of: SignatureKind,
27    },
28    Encoding,
29    ExtraHeaders,
30    Message,
31}
32
33/// Lifecycle
34impl<'a> CommitRefIter<'a> {
35    /// Create a commit iterator from the given `data`, using `object_hash` to know
36    /// what kind of hash to expect for validation.
37    pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> CommitRefIter<'a> {
38        CommitRefIter {
39            data,
40            state: State::default(),
41            hash_kind,
42        }
43    }
44}
45
46/// Access
47impl<'a> CommitRefIter<'a> {
48    /// Parse `data` as commit and return its PGP signature, along with *all non-signature* data as [`SignedData`], or `None`
49    /// if the commit isn't signed. All hashes in `data` are parsed as `object_hash`.
50    ///
51    /// This allows the caller to validate the signature by passing the signed data along with the signature back to the program
52    /// that created it.
53    pub fn signature(
54        data: &'a [u8],
55        hash_kind: gix_hash::Kind,
56    ) -> Result<Option<(Cow<'a, BStr>, SignedData<'a>)>, crate::decode::Error> {
57        let mut signature_and_range = None;
58
59        let raw_tokens = CommitRefIterRaw {
60            data,
61            state: State::default(),
62            offset: 0,
63            hash_kind,
64        };
65        for token in raw_tokens {
66            let token = token?;
67            if let Token::ExtraHeader((name, value)) = &token.token {
68                if *name == signature_field_name(hash_kind) {
69                    // keep track of the signature range alongside the signature data,
70                    // because all but the signature is the signed data.
71                    signature_and_range = Some((value.clone(), token.token_range));
72                    break;
73                }
74            }
75        }
76
77        Ok(signature_and_range.map(|(sig, signature_range)| (sig, SignedData::new(data, signature_range))))
78    }
79
80    /// Returns the object id of this commits tree if it is the first function called and if there is no error in decoding
81    /// the data.
82    ///
83    /// Note that this method must only be called once or else will always return None while consuming a single token.
84    /// Errors are coerced into options, hiding whether there was an error or not. The caller should assume an error if they
85    /// call the method as intended. Such a squelched error cannot be recovered unless the objects data is retrieved and parsed again.
86    /// `next()`.
87    pub fn tree_id(&mut self) -> Result<ObjectId, crate::decode::Error> {
88        let tree_id = self.next().ok_or_else(missing_field)??;
89        Token::try_into_id(tree_id).ok_or_else(missing_field)
90    }
91
92    /// Return all `parent_ids` as iterator.
93    ///
94    /// Parsing errors are ignored quietly.
95    pub fn parent_ids(self) -> impl Iterator<Item = gix_hash::ObjectId> + 'a {
96        self.filter_map(|t| match t {
97            Ok(Token::Parent { id }) => Some(id),
98            _ => None,
99        })
100    }
101
102    /// Returns all signatures, first the author, then the committer, if there is no decoding error.
103    ///
104    /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not
105    /// if not exactly two signatures were iterable.
106    /// Errors are not the common case - if an error needs to be detectable, use this instance as iterator.
107    pub fn signatures(self) -> impl Iterator<Item = gix_actor::SignatureRef<'a>> + 'a {
108        self.filter_map(|t| match t {
109            Ok(Token::Author { signature } | Token::Committer { signature }) => Some(signature),
110            _ => None,
111        })
112    }
113
114    /// Returns the committer signature if there is no decoding error.
115    pub fn committer(mut self) -> Result<gix_actor::SignatureRef<'a>, crate::decode::Error> {
116        self.find_map(|t| match t {
117            Ok(Token::Committer { signature }) => Some(Ok(signature)),
118            Err(err) => Some(Err(err)),
119            _ => None,
120        })
121        .ok_or_else(missing_field)?
122    }
123
124    /// Returns the author signature if there is no decoding error.
125    ///
126    /// It may contain white space surrounding it, and is exactly as parsed.
127    pub fn author(mut self) -> Result<gix_actor::SignatureRef<'a>, crate::decode::Error> {
128        self.find_map(|t| match t {
129            Ok(Token::Author { signature }) => Some(Ok(signature)),
130            Err(err) => Some(Err(err)),
131            _ => None,
132        })
133        .ok_or_else(missing_field)?
134    }
135
136    /// Returns the message if there is no decoding error.
137    ///
138    /// It may contain white space surrounding it, and is exactly as
139    //  parsed.
140    pub fn message(mut self) -> Result<&'a BStr, crate::decode::Error> {
141        self.find_map(|t| match t {
142            Ok(Token::Message(msg)) => Some(Ok(msg)),
143            Err(err) => Some(Err(err)),
144            _ => None,
145        })
146        .transpose()
147        .map(Option::unwrap_or_default)
148    }
149}
150
151fn missing_field() -> crate::decode::Error {
152    crate::decode::empty_error()
153}
154
155impl<'a> CommitRefIter<'a> {
156    #[inline]
157    fn next_inner(
158        mut i: &'a [u8],
159        state: &mut State,
160        hash_kind: gix_hash::Kind,
161    ) -> Result<(&'a [u8], Token<'a>), crate::decode::Error> {
162        let input = &mut i;
163        match Self::next_inner_(input, state, hash_kind) {
164            Ok(token) => Ok((*input, token)),
165            Err(err) => Err(err),
166        }
167    }
168
169    fn next_inner_(
170        input: &mut &'a [u8],
171        state: &mut State,
172        hash_kind: gix_hash::Kind,
173    ) -> Result<Token<'a>, crate::decode::Error> {
174        use State::*;
175        Ok(match state {
176            Tree => {
177                let tree = parse::header_field(input, b"tree", |value| parse::hex_hash(value, hash_kind))?;
178                *state = State::Parents;
179                Token::Tree {
180                    id: ObjectId::from_hex(tree).expect("parsing validation"),
181                }
182            }
183            Parents => {
184                if input.starts_with(b"parent ") {
185                    let parent = parse::header_field(input, b"parent", |value| parse::hex_hash(value, hash_kind))?;
186                    Token::Parent {
187                        id: ObjectId::from_hex(parent).expect("parsing validation"),
188                    }
189                } else {
190                    *state = State::Signature {
191                        of: SignatureKind::Author,
192                    };
193                    Self::next_inner_(input, state, hash_kind)?
194                }
195            }
196            Signature { of } => {
197                let who = *of;
198                let field_name = match of {
199                    SignatureKind::Author => {
200                        *of = SignatureKind::Committer;
201                        &b"author"[..]
202                    }
203                    SignatureKind::Committer => {
204                        *state = State::Encoding;
205                        &b"committer"[..]
206                    }
207                };
208                let signature = parse::header_field(input, field_name, parse::signature)?;
209                match who {
210                    SignatureKind::Author => Token::Author { signature },
211                    SignatureKind::Committer => Token::Committer { signature },
212                }
213            }
214            Encoding => {
215                *state = State::ExtraHeaders;
216                if input.starts_with(b"encoding ") {
217                    let encoding = parse::header_field(input, b"encoding", Ok)?;
218                    Token::Encoding(encoding.as_bstr())
219                } else {
220                    Self::next_inner_(input, state, hash_kind)?
221                }
222            }
223            ExtraHeaders => {
224                if input.starts_with(b"\n") {
225                    *state = State::Message;
226                    Self::next_inner_(input, state, hash_kind)?
227                } else {
228                    let before = *input;
229                    {
230                        let extra_header = parse::any_header_field_multi_line(input)
231                            .map(|(k, o)| (k.as_bstr(), Cow::Owned(o)))
232                            .or_else(|_| {
233                                *input = before;
234                                parse::any_header_field(input).map(|(k, o)| (k.as_bstr(), Cow::Borrowed(o.as_bstr())))
235                            })?;
236                        Token::ExtraHeader(extra_header)
237                    }
238                }
239            }
240            Message => {
241                let message = decode::message(input)?;
242                debug_assert!(
243                    input.is_empty(),
244                    "we should have consumed all data - otherwise iter may go forever"
245                );
246                Token::Message(message)
247            }
248        })
249    }
250}
251
252impl<'a> Iterator for CommitRefIter<'a> {
253    type Item = Result<Token<'a>, crate::decode::Error>;
254
255    fn next(&mut self) -> Option<Self::Item> {
256        if self.data.is_empty() {
257            return None;
258        }
259        match Self::next_inner(self.data, &mut self.state, self.hash_kind) {
260            Ok((data, token)) => {
261                self.data = data;
262                Some(Ok(token))
263            }
264            Err(err) => {
265                self.data = &[];
266                Some(Err(err))
267            }
268        }
269    }
270}
271
272/// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead.
273struct CommitRefIterRaw<'a> {
274    data: &'a [u8],
275    state: State,
276    offset: usize,
277    hash_kind: gix_hash::Kind,
278}
279
280impl<'a> Iterator for CommitRefIterRaw<'a> {
281    type Item = Result<RawToken<'a>, crate::decode::Error>;
282
283    fn next(&mut self) -> Option<Self::Item> {
284        if self.data.is_empty() {
285            return None;
286        }
287        match CommitRefIter::next_inner(self.data, &mut self.state, self.hash_kind) {
288            Ok((remaining, token)) => {
289                let consumed = self.data.len() - remaining.len();
290                let start = self.offset;
291                let end = start + consumed;
292                self.offset = end;
293
294                self.data = remaining;
295                Some(Ok(RawToken {
296                    token,
297                    token_range: start..end,
298                }))
299            }
300            Err(err) => {
301                self.data = &[];
302                Some(Err(err))
303            }
304        }
305    }
306}
307
308/// A combination of a parsed [`Token`] as well as the range of bytes that were consumed to parse it.
309struct RawToken<'a> {
310    /// The parsed token.
311    token: Token<'a>,
312    token_range: Range<usize>,
313}
314
315/// A token returned by the [commit iterator][CommitRefIter].
316#[expect(missing_docs)]
317#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
318pub enum Token<'a> {
319    Tree {
320        id: ObjectId,
321    },
322    Parent {
323        id: ObjectId,
324    },
325    /// A person who authored the content of the commit.
326    Author {
327        signature: gix_actor::SignatureRef<'a>,
328    },
329    /// A person who committed the authors work to the repository.
330    Committer {
331        signature: gix_actor::SignatureRef<'a>,
332    },
333    Encoding(&'a BStr),
334    ExtraHeader((&'a BStr, Cow<'a, BStr>)),
335    Message(&'a BStr),
336}
337
338impl Token<'_> {
339    /// Return the object id of this token if it's a [tree][Token::Tree] or a [parent commit][Token::Parent].
340    pub fn id(&self) -> Option<&oid> {
341        match self {
342            Token::Tree { id } | Token::Parent { id } => Some(id.as_ref()),
343            _ => None,
344        }
345    }
346
347    /// Return the owned object id of this token if it's a [tree][Token::Tree] or a [parent commit][Token::Parent].
348    pub fn try_into_id(self) -> Option<ObjectId> {
349        match self {
350            Token::Tree { id } | Token::Parent { id } => Some(id),
351            _ => None,
352        }
353    }
354}