Skip to content

Commit 2a27780

Browse files
committed
Enable SignatureRef/IdentityRef to preserve raw actor bytes for round-tripping malformed commits
1 parent 2f14246 commit 2a27780

File tree

15 files changed

+107
-14
lines changed

15 files changed

+107
-14
lines changed

gitoxide-core/src/repository/mailmap.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,20 @@ pub fn check(
8585
gix::actor::IdentityRef {
8686
name: "".into(),
8787
email: email.into(),
88+
raw: None,
8889
}
8990
}
9091
};
9192
let resolved = mailmap.resolve_cow(gix::actor::SignatureRef {
9293
name: actor.name,
9394
email: actor.email,
9495
time: Default::default(),
96+
raw: None,
9597
});
9698
let resolved = gix::actor::IdentityRef {
9799
name: resolved.name.as_ref(),
98100
email: resolved.email.as_ref(),
101+
raw: None,
99102
};
100103
buf.clear();
101104
resolved.write_to(&mut buf)?;

gix-actor/src/identity.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ impl<'a> IdentityRef<'a> {
2525
IdentityRef {
2626
name: self.name.trim().as_bstr(),
2727
email: self.email.trim().as_bstr(),
28+
raw: None,
2829
}
2930
}
3031
}
@@ -43,6 +44,10 @@ mod write {
4344
impl IdentityRef<'_> {
4445
/// Serialize this instance to `out` in the git serialization format for signatures (but without timestamp).
4546
pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
47+
if let Some(raw) = self.raw {
48+
out.write_all(raw.as_ref())?;
49+
return Ok(());
50+
}
4651
out.write_all(validated_token(self.name)?)?;
4752
out.write_all(b" ")?;
4853
out.write_all(b"<")?;
@@ -61,13 +66,14 @@ mod impls {
6166
IdentityRef {
6267
name: self.name.as_ref(),
6368
email: self.email.as_ref(),
69+
raw: None,
6470
}
6571
}
6672
}
6773

6874
impl From<IdentityRef<'_>> for Identity {
6975
fn from(other: IdentityRef<'_>) -> Identity {
70-
let IdentityRef { name, email } = other;
76+
let IdentityRef { name, email, .. } = other;
7177
Identity {
7278
name: name.to_owned(),
7379
email: email.to_owned(),
@@ -88,8 +94,15 @@ mod impls {
8894
}
8995

9096
impl<'a> From<SignatureRef<'a>> for IdentityRef<'a> {
91-
fn from(SignatureRef { name, email, time: _ }: SignatureRef<'a>) -> Self {
92-
IdentityRef { name, email }
97+
fn from(
98+
SignatureRef {
99+
name,
100+
email,
101+
raw,
102+
time: _,
103+
}: SignatureRef<'a>,
104+
) -> Self {
105+
IdentityRef { name, email, raw }
93106
}
94107
}
95108
}

gix-actor/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ pub struct IdentityRef<'a> {
4949
///
5050
/// Use [IdentityRef::trim()] or trim manually to be able to clean it up.
5151
pub email: &'a BStr,
52+
/// The raw representation of the identity as encountered in serialized data.
53+
///
54+
/// When present this allows lossless round-tripping of malformed identities which would otherwise
55+
/// trigger the writer's strict validation of `<` and `>` characters.
56+
#[cfg_attr(feature = "serde", serde(borrow))]
57+
pub raw: Option<&'a BStr>,
5258
}
5359

5460
/// A mutable signature that is created by an actor at a certain time.
@@ -92,4 +98,9 @@ pub struct SignatureRef<'a> {
9298
///
9399
/// Use [`SignatureRef::time()`] to decode.
94100
pub time: &'a str,
101+
/// Raw identity part of this signature (`name <email>`), when available.
102+
///
103+
/// See [`IdentityRef::raw`] for details.
104+
#[cfg_attr(feature = "serde", serde(borrow))]
105+
pub raw: Option<&'a BStr>,
95106
}

gix-actor/src/signature/decode.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub(crate) mod function {
3737
name: identity.name,
3838
email: identity.email,
3939
time,
40+
raw: identity.raw,
4041
})
4142
.parse_next(i)
4243
}
@@ -66,6 +67,14 @@ pub(crate) mod function {
6667
let skip_from_left = i[left_delim_idx..].iter().take_while(|b| **b == b'<').count();
6768
let mut name = i[..left_delim_idx].as_bstr();
6869
name = name.strip_suffix(b" ").unwrap_or(name).as_bstr();
70+
let raw_slice =
71+
i.get(..=right_delim_idx)
72+
.map(ByteSlice::as_bstr)
73+
.ok_or(ErrMode::Cut(E::from_input(i).add_context(
74+
i,
75+
&start,
76+
StrContext::Label("Identity slice out of bounds"),
77+
)))?;
6978

7079
let email = i
7180
.get(left_delim_idx + skip_from_left..right_delim_idx - skip_from_right)
@@ -75,8 +84,13 @@ pub(crate) mod function {
7584
StrContext::Label("Skipped parts run into each other"),
7685
)))?
7786
.as_bstr();
87+
let raw = if name.find_byteset(b"<>\n").is_some() || email.find_byteset(b"<>\n").is_some() {
88+
Some(raw_slice)
89+
} else {
90+
None
91+
};
7892
*i = i.get(right_delim_idx + 1..).unwrap_or(&[]);
79-
Ok(IdentityRef { name, email })
93+
Ok(IdentityRef { name, email, raw })
8094
}
8195
}
8296
pub use function::identity;
@@ -100,6 +114,7 @@ mod tests {
100114
name: name.into(),
101115
email: email.into(),
102116
time,
117+
raw: None,
103118
}
104119
}
105120

gix-actor/src/signature/mod.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ mod _ref {
3232
name: self.name.trim().as_bstr(),
3333
email: self.email.trim().as_bstr(),
3434
time: self.time.trim(),
35+
raw: None,
3536
}
3637
}
3738

@@ -40,6 +41,7 @@ mod _ref {
4041
IdentityRef {
4142
name: self.name,
4243
email: self.email,
44+
raw: self.raw,
4345
}
4446
}
4547

@@ -78,6 +80,7 @@ mod convert {
7880
name: self.name.as_ref(),
7981
email: self.email.as_ref(),
8082
time: self.time.to_str(time_buf),
83+
raw: None,
8184
}
8285
}
8386
}
@@ -130,16 +133,26 @@ pub(crate) mod write {
130133
impl SignatureRef<'_> {
131134
/// Serialize this instance to `out` in the git serialization format for actors.
132135
pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
133-
out.write_all(validated_token(self.name)?)?;
136+
if let Some(raw) = self.raw {
137+
out.write_all(raw.as_ref())?;
138+
} else {
139+
out.write_all(validated_token(self.name)?)?;
140+
out.write_all(b" ")?;
141+
out.write_all(b"<")?;
142+
out.write_all(validated_token(self.email)?)?;
143+
out.write_all(b">")?;
144+
}
134145
out.write_all(b" ")?;
135-
out.write_all(b"<")?;
136-
out.write_all(validated_token(self.email)?)?;
137-
out.write_all(b"> ")?;
138146
out.write_all(validated_token(self.time.into())?)
139147
}
140148
/// Computes the number of bytes necessary to serialize this signature
141149
pub fn size(&self) -> usize {
142-
self.name.len() + 2 /* space <*/ + self.email.len() + 2 /* > space */ + self.time.len()
150+
let identity_len = if let Some(raw) = self.raw {
151+
raw.len()
152+
} else {
153+
self.name.len() + 2 /* space <*/ + self.email.len() + 1 /* > */
154+
};
155+
identity_len + 1 /* space */ + self.time.len()
143156
}
144157
}
145158

gix-actor/tests/signature/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ fn signature_ref_round_trips_with_seconds_in_offset() -> Result<(), Box<dyn std:
8383
Ok(())
8484
}
8585

86+
#[test]
87+
fn signature_ref_round_trips_with_malformed_identity() -> Result<(), Box<dyn std::error::Error>> {
88+
let input = b"Gregor Hartmann<gh <Gregor Hartmann<[email protected]>> 1282910542 +0200";
89+
let signature: SignatureRef = gix_actor::SignatureRef::from_bytes::<()>(input).unwrap();
90+
let mut output = Vec::new();
91+
signature.write_to(&mut output)?;
92+
assert_eq!(output.as_bstr(), input.as_bstr());
93+
Ok(())
94+
}
95+
8696
#[test]
8797
fn parse_timestamp_with_trailing_digits() {
8898
let signature = gix_actor::SignatureRef::from_bytes::<()>(b"first last <[email protected]> 1312735823 +051800")
@@ -93,6 +103,7 @@ fn parse_timestamp_with_trailing_digits() {
93103
name: "first last".into(),
94104
email: "[email protected]".into(),
95105
time: "1312735823 +051800",
106+
raw: None,
96107
}
97108
);
98109

@@ -104,6 +115,7 @@ fn parse_timestamp_with_trailing_digits() {
104115
name: "first last".into(),
105116
email: "[email protected]".into(),
106117
time: "1312735823 +0518",
118+
raw: None,
107119
}
108120
);
109121
}
@@ -117,7 +129,8 @@ fn parse_missing_timestamp() {
117129
SignatureRef {
118130
name: "first last".into(),
119131
email: "[email protected]".into(),
120-
time: ""
132+
time: "",
133+
raw: None,
121134
}
122135
);
123136
}

gix-mailmap/src/snapshot/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ impl Snapshot {
154154
}
155155

156156
fn enriched_signature<'a>(
157-
SignatureRef { name, email, time }: SignatureRef<'a>,
157+
SignatureRef { name, email, time, .. }: SignatureRef<'a>,
158158
new: ResolvedSignature<'_>,
159159
) -> Signature<'a> {
160160
match (new.email, new.name) {

gix-object/tests/object/commit/from_bytes.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fn invalid_timestsamp() {
1313
name: b"Name".as_bstr(),
1414
email: b"[email protected]".as_bstr(),
1515
time: "1312735823 +051800",
16+
raw: None,
1617
};
1718
assert_eq!(
1819
CommitRef::from_bytes(&fixture_name("commit", "invalid-timestamp.txt"))
@@ -36,10 +37,12 @@ fn invalid_email_of_committer() {
3637
name: b"Gregor Hartmann".as_bstr(),
3738
email: b"gh <Gregor Hartmann<[email protected]".as_bstr(),
3839
time: "1282910542 +0200",
40+
raw: Some(b"Gregor Hartmann<gh <Gregor Hartmann<[email protected]>>".as_bstr()),
3941
};
42+
let fixture = fixture_name("commit", "invalid-actor.txt");
43+
let commit_ref = CommitRef::from_bytes(&fixture).expect("ignore strangely formed actor format and round-trip");
4044
assert_eq!(
41-
CommitRef::from_bytes(&fixture_name("commit", "invalid-actor.txt"))
42-
.expect("ignore strangely formed actor format"),
45+
commit_ref,
4346
CommitRef {
4447
tree: b"220738fd4199e95a2b244465168366a73ebdf271".as_bstr(),
4548
parents: [b"209fbe2d632761b30b7b17422914e11b93692833".as_bstr()].into(),
@@ -50,6 +53,11 @@ fn invalid_email_of_committer() {
5053
extra_headers: vec![]
5154
}
5255
);
56+
let mut buf = Vec::new();
57+
commit_ref
58+
.write_to(&mut buf)
59+
.expect("serialized commits with malformed actors should succeed");
60+
assert_eq!(buf, fixture);
5361
}
5462

5563
#[test]
@@ -184,6 +192,7 @@ fn pre_epoch() -> crate::Result {
184192
name: "Législateur".into(),
185193
email: "".into(),
186194
time: "-5263834140 +0009",
195+
raw: None,
187196
};
188197
assert_eq!(
189198
CommitRef::from_bytes(&fixture_name("commit", "pre-epoch.txt"))?,
@@ -206,6 +215,7 @@ fn double_dash_special_time_offset() -> crate::Result {
206215
name: "name".into(),
207216
email: "[email protected]".into(),
208217
time: "1288373970 --700",
218+
raw: None,
209219
};
210220
assert_eq!(
211221
CommitRef::from_bytes(&fixture_name("commit", "double-dash-date-offset.txt"))?,
@@ -228,6 +238,7 @@ fn with_trailer() -> crate::Result {
228238
name: "Kim Altintop".into(),
229239
email: "[email protected]".into(),
230240
time: "1631514803 +0200",
241+
raw: None,
231242
};
232243
let backing = fixture_name("commit", "message-with-footer.txt");
233244
let commit = CommitRef::from_bytes(&backing)?;

gix-object/tests/object/commit/message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ mod summary {
236236
name: "name".into(),
237237
email: "email".into(),
238238
time: "0 0000",
239+
raw: None,
239240
};
240241
assert_eq!(
241242
CommitRef {

gix-object/tests/object/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ fn signature(time: &str) -> gix_actor::SignatureRef<'_> {
9696
name: b"Sebastian Thiel".as_bstr(),
9797
email: b"[email protected]".as_bstr(),
9898
time,
99+
raw: None,
99100
}
100101
}
101102

@@ -105,5 +106,6 @@ fn linus_signature(time: &str) -> gix_actor::SignatureRef<'_> {
105106
name: b"Linus Torvalds".as_bstr(),
106107
email: b"[email protected]".as_bstr(),
107108
time,
109+
raw: None,
108110
}
109111
}

0 commit comments

Comments
 (0)