gix_actor/signature/
decode.rs

1pub(crate) mod function {
2    use bstr::ByteSlice;
3    use winnow::{
4        combinator::{opt, separated_pair},
5        error::{AddContext, ErrMode, ParserError, StrContext},
6        prelude::*,
7        stream::{AsChar, Stream},
8        token::take_while,
9    };
10
11    use crate::{IdentityRef, SignatureRef};
12
13    /// Parse a signature from the bytes input `i` using `nom`.
14    pub fn decode<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
15        i: &mut &'a [u8],
16    ) -> ModalResult<SignatureRef<'a>, E> {
17        separated_pair(
18            identity,
19            opt(b" "),
20            opt((
21                take_while(0.., |b: u8| b == b'+' || b == b'-' || b.is_space() || b.is_dec_digit()).map(|v: &[u8]| v),
22            ))
23            .map(|maybe_bytes| {
24                if let Some((bytes,)) = maybe_bytes {
25                    // SAFETY: The parser validated that there are only ASCII characters.
26                    #[allow(unsafe_code)]
27                    unsafe {
28                        std::str::from_utf8_unchecked(bytes)
29                    }
30                } else {
31                    ""
32                }
33            }),
34        )
35        .context(StrContext::Expected("<name> <<email>> <timestamp> <+|-><HHMM>".into()))
36        .map(|(identity, time)| SignatureRef {
37            name: identity.name,
38            email: identity.email,
39            time,
40        })
41        .parse_next(i)
42    }
43
44    /// Parse an identity from the bytes input `i` (like `name <email>`) using `nom`.
45    pub fn identity<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
46        i: &mut &'a [u8],
47    ) -> ModalResult<IdentityRef<'a>, E> {
48        let start = i.checkpoint();
49        let eol_idx = i.find_byte(b'\n').unwrap_or(i.len());
50        let right_delim_idx = i[..eol_idx]
51            .rfind_byte(b'>')
52            .ok_or(ErrMode::Cut(E::from_input(i).add_context(
53                i,
54                &start,
55                StrContext::Label("Closing '>' not found"),
56            )))?;
57        let i_name_and_email = &i[..right_delim_idx];
58        let skip_from_right = i_name_and_email.iter().rev().take_while(|b| **b == b'>').count();
59        let left_delim_idx = i_name_and_email
60            .find_byte(b'<')
61            .ok_or(ErrMode::Cut(E::from_input(i).add_context(
62                &i_name_and_email,
63                &start,
64                StrContext::Label("Opening '<' not found"),
65            )))?;
66        let skip_from_left = i[left_delim_idx..].iter().take_while(|b| **b == b'<').count();
67        let mut name = i[..left_delim_idx].as_bstr();
68        name = name.strip_suffix(b" ").unwrap_or(name).as_bstr();
69
70        let email = i
71            .get(left_delim_idx + skip_from_left..right_delim_idx - skip_from_right)
72            .ok_or(ErrMode::Cut(E::from_input(i).add_context(
73                &i_name_and_email,
74                &start,
75                StrContext::Label("Skipped parts run into each other"),
76            )))?
77            .as_bstr();
78        *i = i.get(right_delim_idx + 1..).unwrap_or(&[]);
79        Ok(IdentityRef { name, email })
80    }
81}
82pub use function::identity;
83
84#[cfg(test)]
85mod tests {
86    mod parse_signature {
87        use gix_testtools::to_bstr_err;
88        use winnow::prelude::*;
89
90        use crate::{signature, SignatureRef};
91
92        fn decode<'i>(
93            i: &mut &'i [u8],
94        ) -> ModalResult<SignatureRef<'i>, winnow::error::TreeError<&'i [u8], winnow::error::StrContext>> {
95            signature::decode.parse_next(i)
96        }
97
98        fn signature(name: &'static str, email: &'static str, time: &'static str) -> SignatureRef<'static> {
99            SignatureRef {
100                name: name.into(),
101                email: email.into(),
102                time,
103            }
104        }
105
106        #[test]
107        fn tz_minus() {
108            let actual = decode
109                .parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0230")
110                .expect("parse to work")
111                .1;
112            assert_eq!(
113                actual,
114                signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 -0230")
115            );
116            assert_eq!(actual.seconds(), 1528473343);
117            assert_eq!(
118                actual.time().expect("valid"),
119                gix_date::Time {
120                    seconds: 1528473343,
121                    offset: -9000,
122                }
123            );
124        }
125
126        #[test]
127        fn tz_plus() {
128            assert_eq!(
129                decode
130                    .parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 +0230")
131                    .expect("parse to work")
132                    .1,
133                signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 +0230")
134            );
135        }
136
137        #[test]
138        fn email_with_space() {
139            assert_eq!(
140                decode
141                    .parse_peek(b"Sebastian Thiel <\tbyronimo@gmail.com > 1528473343 +0230")
142                    .expect("parse to work")
143                    .1,
144                signature("Sebastian Thiel", "\tbyronimo@gmail.com ", "1528473343 +0230")
145            );
146        }
147
148        #[test]
149        fn negative_offset_0000() {
150            assert_eq!(
151                decode
152                    .parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0000")
153                    .expect("parse to work")
154                    .1,
155                signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 -0000")
156            );
157        }
158
159        #[test]
160        fn negative_offset_double_dash() {
161            assert_eq!(
162                decode
163                    .parse_peek(b"name <name@example.com> 1288373970 --700")
164                    .expect("parse to work")
165                    .1,
166                signature("name", "name@example.com", "1288373970 --700")
167            );
168        }
169
170        #[test]
171        fn empty_name_and_email() {
172            assert_eq!(
173                decode.parse_peek(b" <> 12345 -1215").expect("parse to work").1,
174                signature("", "", "12345 -1215")
175            );
176        }
177
178        #[test]
179        fn invalid_signature() {
180            assert_eq!(
181                        decode.parse_peek(b"hello < 12345 -1215")
182                            .map_err(to_bstr_err)
183                            .expect_err("parse fails as > is missing")
184                            .to_string(),
185                        " at 'hello < 12345 -1215'\n  0: invalid Closing '>' not found at 'hello < 12345 -1215'\n  1: expected `<name> <<email>> <timestamp> <+|-><HHMM>` at 'hello < 12345 -1215'\n"
186                    );
187        }
188
189        #[test]
190        fn invalid_time() {
191            assert_eq!(
192                decode.parse_peek(b"hello <> abc -1215").expect("parse to work").1,
193                signature("hello", "", "")
194            );
195        }
196    }
197}