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
16pub 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 }; 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}