Skip to content

Commit 2c0c32a

Browse files
pierrechevalier83Pierre Chevalier
authored and
Pierre Chevalier
committed
fix!: Make Signature roundtrip
By storing the raw bytes from Git instead of parsing them into a `gix_date::Time` from the start, we are able to roundtrip between Git and gix-actor even with creatively formatted Git data. Also add a test case that shows a time offset seen in the wild which would have failed to round-trip without this change.
1 parent b814043 commit 2c0c32a

File tree

4 files changed

+51
-76
lines changed

4 files changed

+51
-76
lines changed

gix-actor/src/lib.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
///
1414
/// For convenience to allow using `bstr` without adding it to own cargo manifest.
1515
pub use bstr;
16-
use bstr::{BStr, BString};
16+
use bstr::{BStr, BString, ByteSlice};
1717
/// The re-exported `gix-date` crate.
1818
///
1919
/// For convenience to allow using `gix-date` without adding it to own cargo manifest.
2020
pub use gix_date as date;
21-
use gix_date::Time;
2221

2322
mod identity;
2423
///
@@ -68,9 +67,15 @@ pub struct Signature {
6867
/// Use [SignatureRef::trim()] or trim manually to be able to clean it up.
6968
pub email: BString,
7069
/// The time stamp at which the signature is performed.
71-
pub time: Time,
70+
pub time: BString,
7271
}
7372

73+
impl Signature {
74+
/// Seconds since unix epoch from the time
75+
pub fn seconds(&self) -> gix_date::SecondsSinceUnixEpoch {
76+
SignatureRef::from(self).seconds()
77+
}
78+
}
7479
/// A immutable signature is created by an actor at a certain time.
7580
///
7681
/// Note that this is not a cryptographical signature.
@@ -87,5 +92,17 @@ pub struct SignatureRef<'a> {
8792
/// Use [SignatureRef::trim()] or trim manually to be able to clean it up.
8893
pub email: &'a BStr,
8994
/// The time stamp at which the signature was performed.
90-
pub time: gix_date::Time,
95+
pub time: &'a BStr,
96+
}
97+
98+
impl SignatureRef<'_> {
99+
/// Seconds since unix epoch from the time
100+
pub fn seconds(&self) -> gix_date::SecondsSinceUnixEpoch {
101+
self.time
102+
.split(u8::is_ascii_whitespace)
103+
.next()
104+
.and_then(|i| i.to_str().ok())
105+
.and_then(|s| s.parse().ok())
106+
.unwrap_or(Default::default())
107+
}
91108
}

gix-actor/src/signature/decode.rs

Lines changed: 17 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
pub(crate) mod function {
22
use crate::{IdentityRef, SignatureRef};
33
use bstr::ByteSlice;
4-
use gix_date::{time::Sign, OffsetInSeconds, SecondsSinceUnixEpoch, Time};
5-
use gix_utils::btoi::to_signed;
64
use winnow::error::ErrMode;
75
use winnow::stream::Stream;
86
use winnow::{
9-
combinator::{alt, opt, separated_pair, terminated},
7+
combinator::{opt, separated_pair},
108
error::{AddContext, ParserError, StrContext},
119
prelude::*,
1210
stream::AsChar,
13-
token::{take, take_until, take_while},
11+
token::take_while,
1412
};
1513

16-
const SPACE: &[u8] = b" ";
17-
1814
/// Parse a signature from the bytes input `i` using `nom`.
1915
pub fn decode<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
2016
i: &mut &'a [u8],
@@ -23,36 +19,13 @@ pub(crate) mod function {
2319
identity,
2420
opt(b" "),
2521
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),
22+
take_while(0.., |b: u8| b == b'+' || b == b'-' || b.is_space() || b.is_dec_digit()).map(|v: &[u8]| v),
4123
))
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-
}
24+
.map(|maybe_bytes| {
25+
if let Some((bytes,)) = maybe_bytes {
26+
bytes.into()
5427
} else {
55-
Time::new(0, 0)
28+
b"".into()
5629
}
5730
}),
5831
)
@@ -109,29 +82,22 @@ pub use function::identity;
10982
mod tests {
11083
mod parse_signature {
11184
use bstr::ByteSlice;
112-
use gix_date::{time::Sign, OffsetInSeconds, SecondsSinceUnixEpoch};
11385
use gix_testtools::to_bstr_err;
11486
use winnow::prelude::*;
11587

116-
use crate::{signature, SignatureRef, Time};
88+
use crate::{signature, SignatureRef};
11789

11890
fn decode<'i>(
11991
i: &mut &'i [u8],
12092
) -> ModalResult<SignatureRef<'i>, winnow::error::TreeError<&'i [u8], winnow::error::StrContext>> {
12193
signature::decode.parse_next(i)
12294
}
12395

124-
fn signature(
125-
name: &'static str,
126-
email: &'static str,
127-
seconds: SecondsSinceUnixEpoch,
128-
sign: Sign,
129-
offset: OffsetInSeconds,
130-
) -> SignatureRef<'static> {
96+
fn signature(name: &'static str, email: &'static str, time: &'static str) -> SignatureRef<'static> {
13197
SignatureRef {
13298
name: name.as_bytes().as_bstr(),
13399
email: email.as_bytes().as_bstr(),
134-
time: Time { seconds, offset, sign },
100+
time: time.as_bytes().as_bstr(),
135101
}
136102
}
137103

@@ -142,7 +108,7 @@ mod tests {
142108
.parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0230")
143109
.expect("parse to work")
144110
.1,
145-
signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, -9000)
111+
signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 -0230")
146112
);
147113
}
148114

@@ -153,7 +119,7 @@ mod tests {
153119
.parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 +0230")
154120
.expect("parse to work")
155121
.1,
156-
signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Plus, 9000)
122+
signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 +0230")
157123
);
158124
}
159125

@@ -164,7 +130,7 @@ mod tests {
164130
.parse_peek(b"Sebastian Thiel <\tbyronimo@gmail.com > 1528473343 +0230")
165131
.expect("parse to work")
166132
.1,
167-
signature("Sebastian Thiel", "\tbyronimo@gmail.com ", 1528473343, Sign::Plus, 9000)
133+
signature("Sebastian Thiel", "\tbyronimo@gmail.com ", "1528473343 +0230")
168134
);
169135
}
170136

@@ -175,7 +141,7 @@ mod tests {
175141
.parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0000")
176142
.expect("parse to work")
177143
.1,
178-
signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, 0)
144+
signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 -0000")
179145
);
180146
}
181147

@@ -186,15 +152,15 @@ mod tests {
186152
.parse_peek(b"name <name@example.com> 1288373970 --700")
187153
.expect("parse to work")
188154
.1,
189-
signature("name", "name@example.com", 1288373970, Sign::Minus, -252000)
155+
signature("name", "name@example.com", "1288373970 --700")
190156
);
191157
}
192158

193159
#[test]
194160
fn empty_name_and_email() {
195161
assert_eq!(
196162
decode.parse_peek(b" <> 12345 -1215").expect("parse to work").1,
197-
signature("", "", 12345, Sign::Minus, -44100)
163+
signature("", "", "12345 -1215")
198164
);
199165
}
200166

@@ -213,7 +179,7 @@ mod tests {
213179
fn invalid_time() {
214180
assert_eq!(
215181
decode.parse_peek(b"hello <> abc -1215").expect("parse to work").1,
216-
signature("hello", "", 0, Sign::Plus, 0)
182+
signature("hello", "", "")
217183
);
218184
}
219185
}

gix-actor/src/signature/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ mod _ref {
1818
Signature {
1919
name: self.name.to_owned(),
2020
email: self.email.to_owned(),
21-
time: self.time,
21+
time: self.time.to_owned(),
2222
}
2323
}
2424

@@ -27,7 +27,7 @@ mod _ref {
2727
SignatureRef {
2828
name: self.name.trim().as_bstr(),
2929
email: self.email.trim().as_bstr(),
30-
time: self.time,
30+
time: self.time.trim().as_bstr(),
3131
}
3232
}
3333

@@ -50,7 +50,7 @@ mod convert {
5050
SignatureRef {
5151
name: self.name.as_ref(),
5252
email: self.email.as_ref(),
53-
time: self.time,
53+
time: self.time.as_ref(),
5454
}
5555
}
5656
}
@@ -61,7 +61,7 @@ mod convert {
6161
Signature {
6262
name: name.to_owned(),
6363
email: email.to_owned(),
64-
time,
64+
time: time.to_owned(),
6565
}
6666
}
6767
}
@@ -112,11 +112,11 @@ pub(crate) mod write {
112112
out.write_all(b"<")?;
113113
out.write_all(validated_token(self.email)?)?;
114114
out.write_all(b"> ")?;
115-
self.time.write_to(out)
115+
out.write_all(validated_token(self.time)?)
116116
}
117117
/// Computes the number of bytes necessary to serialize this signature
118118
pub fn size(&self) -> usize {
119-
self.name.len() + 2 /* space <*/ + self.email.len() + 2 /* > space */ + self.time.size()
119+
self.name.len() + 2 /* space <*/ + self.email.len() + 2 /* > space */ + self.time.len()
120120
}
121121
}
122122

gix-actor/tests/signature/mod.rs

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
mod write_to {
22
mod invalid {
33
use gix_actor::Signature;
4-
use gix_date::{time::Sign, Time};
54

65
#[test]
76
fn name() {
87
let signature = Signature {
98
name: "invalid < middlename".into(),
109
email: "ok".into(),
11-
time: default_time(),
10+
time: "0 0000".into(),
1211
};
1312
assert_eq!(
1413
format!("{:?}", signature.write_to(&mut Vec::new())),
@@ -21,7 +20,7 @@ mod write_to {
2120
let signature = Signature {
2221
name: "ok".into(),
2322
email: "server>.example.com".into(),
24-
time: default_time(),
23+
time: "0 0000".into(),
2524
};
2625
assert_eq!(
2726
format!("{:?}", signature.write_to(&mut Vec::new())),
@@ -34,21 +33,13 @@ mod write_to {
3433
let signature = Signature {
3534
name: "hello\nnewline".into(),
3635
email: "name@example.com".into(),
37-
time: default_time(),
36+
time: "0 0000".into(),
3837
};
3938
assert_eq!(
4039
format!("{:?}", signature.write_to(&mut Vec::new())),
4140
"Err(Custom { kind: Other, error: IllegalCharacter })"
4241
);
4342
}
44-
45-
fn default_time() -> Time {
46-
Time {
47-
seconds: 0,
48-
offset: 0,
49-
sign: Sign::Plus,
50-
}
51-
}
5243
}
5344
}
5445

@@ -68,6 +59,7 @@ fn round_trip() -> Result<(), Box<dyn std::error::Error>> {
6859
static DEFAULTS: &[&[u8]] = &[
6960
b"Sebastian Thiel <byronimo@gmail.com> 1 -0030",
7061
b"Sebastian Thiel <byronimo@gmail.com> -1500 -0030",
62+
b"Sebastian Thiel <byronimo@gmail.com> 1313584730 +051800", // Seen in the wild
7163
".. ☺️Sebastian 王知明 Thiel🙌 .. <byronimo@gmail.com> 1528473343 +0230".as_bytes(),
7264
b".. whitespace \t is explicitly allowed - unicode aware trimming must be done elsewhere <byronimo@gmail.com> 1528473343 +0230"
7365
];
@@ -90,7 +82,7 @@ fn parse_timestamp_with_trailing_digits() {
9082
SignatureRef {
9183
name: "first last".into(),
9284
email: "name@example.com".into(),
93-
time: gix_actor::date::Time::new(1312735823, 0),
85+
time: "1312735823 +051800".into(),
9486
}
9587
);
9688

@@ -101,7 +93,7 @@ fn parse_timestamp_with_trailing_digits() {
10193
SignatureRef {
10294
name: "first last".into(),
10395
email: "name@example.com".into(),
104-
time: gix_actor::date::Time::new(1312735823, 19080),
96+
time: "1312735823 +0518".into(),
10597
}
10698
);
10799
}
@@ -115,7 +107,7 @@ fn parse_missing_timestamp() {
115107
SignatureRef {
116108
name: "first last".into(),
117109
email: "name@example.com".into(),
118-
time: gix_actor::date::Time::new(0, 0),
110+
time: "".into(),
119111
}
120112
);
121113
}

0 commit comments

Comments
 (0)