Skip to content

Commit 40be214

Browse files
committed
fix: make actor parsing even more lenient (#1438)
1 parent 6c5861d commit 40be214

File tree

2 files changed

+69
-13
lines changed

2 files changed

+69
-13
lines changed

gix-actor/src/signature/decode.rs

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
pub(crate) mod function {
2+
use crate::{IdentityRef, SignatureRef};
23
use bstr::ByteSlice;
34
use gix_date::{time::Sign, OffsetInSeconds, SecondsSinceUnixEpoch, Time};
45
use gix_utils::btoi::to_signed;
6+
use winnow::error::{ErrMode, ErrorKind};
7+
use winnow::stream::Stream;
58
use winnow::{
69
combinator::{alt, separated_pair, terminated},
710
error::{AddContext, ParserError, StrContext},
@@ -10,8 +13,6 @@ pub(crate) mod function {
1013
token::{take, take_until, take_while},
1114
};
1215

13-
use crate::{IdentityRef, SignatureRef};
14-
1516
const SPACE: &[u8] = b" ";
1617

1718
/// Parse a signature from the bytes input `i` using `nom`.
@@ -64,16 +65,47 @@ pub(crate) mod function {
6465
pub fn identity<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
6566
i: &mut &'a [u8],
6667
) -> PResult<IdentityRef<'a>, E> {
67-
(
68-
terminated(take_until(0.., &b" <"[..]), take(2usize)).context(StrContext::Expected("<name>".into())),
69-
terminated(take_until(0.., &b">"[..]), take(1usize)).context(StrContext::Expected("<email>".into())),
70-
)
71-
.map(|(name, email): (&[u8], &[u8])| IdentityRef {
72-
name: name.as_bstr(),
73-
email: email.as_bstr(),
74-
})
75-
.context(StrContext::Expected("<name> <<email>>".into()))
76-
.parse_next(i)
68+
let start = i.checkpoint();
69+
let eol_idx = i.find_byte(b'\n').unwrap_or(i.len());
70+
let right_delim_idx =
71+
i[..eol_idx]
72+
.rfind_byte(b'>')
73+
.ok_or(ErrMode::Cut(E::from_error_kind(i, ErrorKind::Eof).add_context(
74+
i,
75+
&start,
76+
StrContext::Label("Closing '>' not found"),
77+
)))?;
78+
let i_name_and_email = &i[..right_delim_idx];
79+
let skip_from_right = i_name_and_email
80+
.iter()
81+
.rev()
82+
.take_while(|b| b.is_ascii_whitespace() || **b == b'>')
83+
.count();
84+
let left_delim_idx =
85+
i_name_and_email
86+
.find_byte(b'<')
87+
.ok_or(ErrMode::Cut(E::from_error_kind(i, ErrorKind::Eof).add_context(
88+
&i_name_and_email,
89+
&start,
90+
StrContext::Label("Opening '<' not found"),
91+
)))?;
92+
let skip_from_left = i[left_delim_idx..]
93+
.iter()
94+
.take_while(|b| b.is_ascii_whitespace() || **b == b'<')
95+
.count();
96+
let mut name = i[..left_delim_idx].as_bstr();
97+
name = name.strip_suffix(b" ").unwrap_or(name).as_bstr();
98+
99+
let email = i
100+
.get(left_delim_idx + skip_from_left..right_delim_idx - skip_from_right)
101+
.ok_or(ErrMode::Cut(E::from_error_kind(i, ErrorKind::Eof).add_context(
102+
&i_name_and_email,
103+
&start,
104+
StrContext::Label("Skipped parts run into each other"),
105+
)))?
106+
.as_bstr();
107+
*i = i.get(right_delim_idx + 1..).unwrap_or(&[]);
108+
Ok(IdentityRef { name, email })
77109
}
78110
}
79111
pub use function::identity;
@@ -167,7 +199,7 @@ mod tests {
167199
.map_err(to_bstr_err)
168200
.expect_err("parse fails as > is missing")
169201
.to_string(),
170-
"in slice at ' 12345 -1215'\n 0: expected `<email>` at ' 12345 -1215'\n 1: expected `<name> <<email>>` at 'hello < 12345 -1215'\n 2: expected `<name> <<email>> <timestamp> <+|-><HHMM>` at 'hello < 12345 -1215'\n"
202+
"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"
171203
);
172204
}
173205

gix-actor/tests/identity/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,27 @@ fn round_trip() -> gix_testtools::Result {
1616
}
1717
Ok(())
1818
}
19+
20+
#[test]
21+
fn lenient_parsing() -> gix_testtools::Result {
22+
for input in [
23+
"First Last<<fl <First Last<[email protected] >> >",
24+
"First Last<fl <First Last<[email protected]>>\n",
25+
] {
26+
let identity = gix_actor::IdentityRef::from_bytes::<()>(input.as_bytes()).unwrap();
27+
assert_eq!(identity.name, "First Last");
28+
assert_eq!(
29+
identity.email, "fl <First Last<[email protected]",
30+
"extra trailing and leading angled parens are stripped"
31+
);
32+
let signature: Identity = identity.into();
33+
let mut output = Vec::new();
34+
let err = signature.write_to(&mut output).unwrap_err();
35+
assert_eq!(
36+
err.to_string(),
37+
"Signature name or email must not contain '<', '>' or \\n",
38+
"this isn't roundtrippable as the name is technically incorrect - must not contain brackets"
39+
);
40+
}
41+
Ok(())
42+
}

0 commit comments

Comments
 (0)