git_actor/signature/
decode.rs

1use bstr::ByteSlice;
2use btoi::btoi;
3use nom::{
4    branch::alt,
5    bytes::complete::{tag, take, take_until, take_while_m_n},
6    character::is_digit,
7    error::{context, ContextError, ParseError},
8    sequence::{terminated, tuple},
9    IResult,
10};
11
12use crate::{Sign, SignatureRef, Time};
13
14const SPACE: &[u8] = b" ";
15
16/// Parse a signature from the bytes input `i` using `nom`.
17pub fn decode<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
18    i: &'a [u8],
19) -> IResult<&'a [u8], SignatureRef<'a>, E> {
20    let (i, (name, email, time, tzsign, hours, minutes)) = context(
21        "<name> <<email>> <timestamp> <+|-><HHMM>",
22        tuple((
23            context("<name>", terminated(take_until(&b" <"[..]), take(2usize))),
24            context("<email>", terminated(take_until(&b"> "[..]), take(2usize))),
25            context("<timestamp>", |i| {
26                terminated(take_until(SPACE), take(1usize))(i).and_then(|(i, v)| {
27                    btoi::<u32>(v)
28                        .map(|v| (i, v))
29                        .map_err(|_| nom::Err::Error(E::from_error_kind(i, nom::error::ErrorKind::MapRes)))
30                })
31            }),
32            context("+|-", alt((tag(b"-"), tag(b"+")))),
33            context("HH", |i| {
34                take_while_m_n(2usize, 2, is_digit)(i).and_then(|(i, v)| {
35                    btoi::<i32>(v)
36                        .map(|v| (i, v))
37                        .map_err(|_| nom::Err::Error(E::from_error_kind(i, nom::error::ErrorKind::MapRes)))
38                })
39            }),
40            context("MM", |i| {
41                take_while_m_n(2usize, 2, is_digit)(i).and_then(|(i, v)| {
42                    btoi::<i32>(v)
43                        .map(|v| (i, v))
44                        .map_err(|_| nom::Err::Error(E::from_error_kind(i, nom::error::ErrorKind::MapRes)))
45                })
46            }),
47        )),
48    )(i)?;
49
50    debug_assert!(tzsign[0] == b'-' || tzsign[0] == b'+', "parser assure it's +|- only");
51    let sign = if tzsign[0] == b'-' { Sign::Minus } else { Sign::Plus }; //
52    let offset = (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 };
53
54    Ok((
55        i,
56        SignatureRef {
57            name: name.as_bstr(),
58            email: email.as_bstr(),
59            time: Time {
60                seconds_since_unix_epoch: time,
61                offset_in_seconds: offset,
62                sign,
63            },
64        },
65    ))
66}
67
68#[cfg(test)]
69mod tests {
70    mod parse_signature {
71        use bstr::ByteSlice;
72        use git_testtools::to_bstr_err;
73        use nom::IResult;
74
75        use crate::{signature, Sign, SignatureRef, Time};
76
77        fn decode(i: &[u8]) -> IResult<&[u8], SignatureRef<'_>, nom::error::VerboseError<&[u8]>> {
78            signature::decode(i)
79        }
80
81        fn signature(
82            name: &'static str,
83            email: &'static str,
84            time: u32,
85            sign: Sign,
86            offset: i32,
87        ) -> SignatureRef<'static> {
88            SignatureRef {
89                name: name.as_bytes().as_bstr(),
90                email: email.as_bytes().as_bstr(),
91                time: Time {
92                    seconds_since_unix_epoch: time,
93                    offset_in_seconds: offset,
94                    sign,
95                },
96            }
97        }
98
99        #[test]
100        fn tz_minus() {
101            assert_eq!(
102                decode(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0230")
103                    .expect("parse to work")
104                    .1,
105                signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, -9000)
106            );
107        }
108
109        #[test]
110        fn tz_plus() {
111            assert_eq!(
112                decode(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 +0230")
113                    .expect("parse to work")
114                    .1,
115                signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Plus, 9000)
116            );
117        }
118
119        #[test]
120        fn negative_offset_0000() {
121            assert_eq!(
122                decode(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0000")
123                    .expect("parse to work")
124                    .1,
125                signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, 0)
126            );
127        }
128
129        #[test]
130        fn empty_name_and_email() {
131            assert_eq!(
132                decode(b" <> 12345 -1215").expect("parse to work").1,
133                signature("", "", 12345, Sign::Minus, -44100)
134            );
135        }
136
137        #[test]
138        fn invalid_signature() {
139            assert_eq!(
140                        decode(b"hello < 12345 -1215")
141                            .map_err(to_bstr_err)
142                            .expect_err("parse fails as > is missing")
143                            .to_string(),
144                        "Parse error:\nTakeUntil at:  12345 -1215\nin section '<email>', at:  12345 -1215\nin section '<name> <<email>> <timestamp> <+|-><HHMM>', at: hello < 12345 -1215\n"
145                    );
146        }
147
148        #[test]
149        fn invalid_time() {
150            assert_eq!(
151                        decode(b"hello <> abc -1215")
152                            .map_err(to_bstr_err)
153                            .expect_err("parse fails as > is missing")
154                            .to_string(),
155                        "Parse error:\nMapRes at: -1215\nin section '<timestamp>', at: abc -1215\nin section '<name> <<email>> <timestamp> <+|-><HHMM>', at: hello <> abc -1215\n"
156                    );
157        }
158    }
159}