gix_actor/signature/
decode.rs

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