Skip to main content

gix_object/commit/
ref_iter.rs

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