gix_actor/signature/
decode.rs1pub(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 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 #[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 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}