Skip to content

Commit 7d6d5e1

Browse files
committed
fix!: expose raw commit/tag actor headers for round-tripping
1 parent 2f14246 commit 7d6d5e1

File tree

7 files changed

+104
-28
lines changed

7 files changed

+104
-28
lines changed

gix-object/src/commit/decode.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ pub fn commit<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
4040
.context(StrContext::Expected(
4141
"zero or more 'parent <40 lowercase hex char>'".into(),
4242
)),
43-
(|i: &mut _| parse::header_field(i, b"author", parse::signature))
43+
(|i: &mut _| parse::header_field(i, b"author", parse::signature_with_raw))
4444
.context(StrContext::Expected("author <signature>".into())),
45-
(|i: &mut _| parse::header_field(i, b"committer", parse::signature))
45+
(|i: &mut _| parse::header_field(i, b"committer", parse::signature_with_raw))
4646
.context(StrContext::Expected("committer <signature>".into())),
4747
opt(|i: &mut _| parse::header_field(i, b"encoding", take_till(0.., NL)))
4848
.context(StrContext::Expected("encoding <encoding>".into())),
@@ -60,14 +60,18 @@ pub fn commit<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
6060
terminated(message, eof),
6161
)
6262
.map(
63-
|(tree, parents, author, committer, encoding, extra_headers, message)| CommitRef {
64-
tree,
65-
parents: SmallVec::from(parents),
66-
author,
67-
committer,
68-
encoding: encoding.map(ByteSlice::as_bstr),
69-
message,
70-
extra_headers,
63+
|(tree, parents, (author, author_raw), (committer, committer_raw), encoding, extra_headers, message)| {
64+
CommitRef {
65+
tree,
66+
parents: SmallVec::from(parents),
67+
author,
68+
author_raw,
69+
committer,
70+
committer_raw,
71+
encoding: encoding.map(ByteSlice::as_bstr),
72+
message,
73+
extra_headers,
74+
}
7175
},
7276
)
7377
.parse_next(i)

gix-object/src/commit/write.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ use bstr::ByteSlice;
44

55
use crate::{encode, encode::NL, Commit, CommitRef, Kind};
66

7+
fn signature_requires_raw(signature: &gix_actor::SignatureRef<'_>) -> bool {
8+
signature.name.find_byteset(b"<>\n").is_some() || signature.email.find_byteset(b"<>\n").is_some()
9+
}
10+
11+
fn signature_len(signature: &gix_actor::SignatureRef<'_>, raw: &bstr::BStr) -> usize {
12+
if signature_requires_raw(signature) {
13+
raw.len()
14+
} else {
15+
signature.size()
16+
}
17+
}
18+
19+
fn write_signature(
20+
mut out: &mut dyn io::Write,
21+
field: &[u8],
22+
signature: &gix_actor::SignatureRef<'_>,
23+
raw: &bstr::BStr,
24+
) -> io::Result<()> {
25+
if signature_requires_raw(signature) {
26+
encode::trusted_header_field(field, raw.as_ref(), &mut out)
27+
} else {
28+
encode::trusted_header_signature(field, signature, &mut out)
29+
}
30+
}
31+
732
impl crate::WriteTo for Commit {
833
/// Serializes this instance to `out` in the git serialization format.
934
fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> {
@@ -58,8 +83,8 @@ impl crate::WriteTo for CommitRef<'_> {
5883
for parent in self.parents() {
5984
encode::trusted_header_id(b"parent", &parent, &mut out)?;
6085
}
61-
encode::trusted_header_signature(b"author", &self.author, &mut out)?;
62-
encode::trusted_header_signature(b"committer", &self.committer, &mut out)?;
86+
write_signature(&mut out, b"author", &self.author, self.author_raw)?;
87+
write_signature(&mut out, b"committer", &self.committer, self.committer_raw)?;
6388
if let Some(encoding) = self.encoding.as_ref() {
6489
encode::header_field(b"encoding", encoding, &mut out)?;
6590
}
@@ -78,8 +103,8 @@ impl crate::WriteTo for CommitRef<'_> {
78103
let hash_in_hex = self.tree().kind().len_in_hex();
79104
(b"tree".len() + 1 /* space */ + hash_in_hex + 1 /* nl */
80105
+ self.parents.iter().count() * (b"parent".len() + 1 /* space */ + hash_in_hex + 1 /* nl */)
81-
+ b"author".len() + 1 /* space */ + self.author.size() + 1 /* nl */
82-
+ b"committer".len() + 1 /* space */ + self.committer.size() + 1 /* nl */
106+
+ b"author".len() + 1 /* space */ + signature_len(&self.author, self.author_raw) + 1 /* nl */
107+
+ b"committer".len() + 1 /* space */ + signature_len(&self.committer, self.committer_raw) + 1 /* nl */
83108
+ self
84109
.encoding
85110
.as_ref()

gix-object/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,19 @@ pub struct CommitRef<'a> {
9292
///
9393
/// Use the [`author()`](CommitRef::author()) method to received a trimmed version of it.
9494
pub author: gix_actor::SignatureRef<'a>,
95+
/// The raw author header as encountered during parsing.
96+
///
97+
/// This is used to preserve otherwise invalid identities during serialization.
98+
pub author_raw: &'a BStr,
9599
/// Who committed this commit. Name and email might contain whitespace and are not trimmed to ensure round-tripping.
96100
///
97101
/// Use the [`committer()`](CommitRef::committer()) method to received a trimmed version of it.
98102
///
99103
/// This may be different from the `author` in case the author couldn't write to the repository themselves and
100104
/// is commonly encountered with contributed commits.
101105
pub committer: gix_actor::SignatureRef<'a>,
106+
/// The raw committer header as encountered during parsing.
107+
pub committer_raw: &'a BStr,
102108
/// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493).
103109
pub encoding: Option<&'a BStr>,
104110
/// The commit message documenting the change.
@@ -152,6 +158,8 @@ pub struct TagRef<'a> {
152158
pub name: &'a BStr,
153159
/// The author of the tag.
154160
pub tagger: Option<gix_actor::SignatureRef<'a>>,
161+
/// The raw tagger header as encountered during parsing.
162+
pub tagger_raw: Option<&'a BStr>,
155163
/// The message describing this release.
156164
pub message: &'a BStr,
157165
/// A cryptographic signature over the entire content of the serialized tag object thus far.

gix-object/src/object/convert.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ impl From<TagRef<'_>> for Tag {
88
target_kind,
99
message,
1010
tagger: signature,
11+
tagger_raw: _,
1112
pgp_signature,
1213
} = other;
1314
Tag {
@@ -27,7 +28,9 @@ impl From<CommitRef<'_>> for Commit {
2728
tree,
2829
parents,
2930
author,
31+
author_raw: _,
3032
committer,
33+
committer_raw: _,
3134
encoding,
3235
message,
3336
extra_headers,

gix-object/src/parse.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,15 @@ pub fn hex_hash<'a, E: ParserError<&'a [u8]>>(i: &mut &'a [u8]) -> ModalResult<&
6969
pub(crate) fn signature<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
7070
i: &mut &'a [u8],
7171
) -> ModalResult<gix_actor::SignatureRef<'a>, E> {
72-
gix_actor::signature::decode(i)
72+
signature_with_raw(i).map(|(signature, _)| signature)
73+
}
74+
75+
pub(crate) fn signature_with_raw<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
76+
i: &mut &'a [u8],
77+
) -> ModalResult<(gix_actor::SignatureRef<'a>, &'a BStr), E> {
78+
let original = *i;
79+
gix_actor::signature::decode(i).map(|signature| {
80+
let consumed = original.len() - i.len();
81+
(signature, original[..consumed].as_bstr())
82+
})
7383
}

gix-object/src/tag/decode.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,24 @@ pub fn git_tag<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
1919
.context(StrContext::Expected("type <object kind>".into())),
2020
(|i: &mut _| parse::header_field(i, b"tag", take_while(1.., |b| b != NL[0])))
2121
.context(StrContext::Expected("tag <version>".into())),
22-
opt(|i: &mut _| parse::header_field(i, b"tagger", parse::signature))
22+
opt(|i: &mut _| parse::header_field(i, b"tagger", parse::signature_with_raw))
2323
.context(StrContext::Expected("tagger <signature>".into())),
2424
terminated(message, eof),
2525
)
26-
.map(
27-
|(target, kind, tag_version, signature, (message, pgp_signature))| TagRef {
26+
.map(|(target, kind, tag_version, signature, (message, pgp_signature))| {
27+
let (tagger, tagger_raw) = signature
28+
.map(|(sig, raw)| (Some(sig), Some(raw)))
29+
.unwrap_or((None, None));
30+
TagRef {
2831
target,
2932
name: tag_version.as_bstr(),
3033
target_kind: kind,
3134
message,
32-
tagger: signature,
35+
tagger,
36+
tagger_raw,
3337
pgp_signature,
34-
},
35-
)
38+
}
39+
})
3640
.parse_next(i)
3741
}
3842

gix-object/src/tag/write.rs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
use std::io;
22

3-
use bstr::BStr;
3+
use bstr::{BStr, ByteSlice};
44
use gix_date::parse::TimeBuf;
55

66
use crate::{encode, encode::NL, Kind, Tag, TagRef};
77

8+
fn signature_requires_raw(signature: &gix_actor::SignatureRef<'_>) -> bool {
9+
signature.name.find_byteset(b"<>\n").is_some() || signature.email.find_byteset(b"<>\n").is_some()
10+
}
11+
12+
fn signature_len(signature: &gix_actor::SignatureRef<'_>, raw: &BStr) -> usize {
13+
if signature_requires_raw(signature) {
14+
raw.len()
15+
} else {
16+
signature.size()
17+
}
18+
}
19+
820
/// An Error used in [`Tag::write_to()`][crate::WriteTo::write_to()].
921
#[derive(Debug, thiserror::Error)]
1022
#[allow(missing_docs)]
@@ -64,8 +76,15 @@ impl crate::WriteTo for TagRef<'_> {
6476
encode::trusted_header_field(b"object", self.target, &mut out)?;
6577
encode::trusted_header_field(b"type", self.target_kind.as_bytes(), &mut out)?;
6678
encode::header_field(b"tag", validated_name(self.name)?, &mut out)?;
67-
if let Some(tagger) = &self.tagger {
68-
encode::trusted_header_signature(b"tagger", tagger, &mut out)?;
79+
if let Some(tagger) = self.tagger {
80+
let raw = self
81+
.tagger_raw
82+
.expect("raw tagger data should be present whenever a tagger exists");
83+
if signature_requires_raw(&tagger) {
84+
encode::trusted_header_field(b"tagger", raw.as_ref(), &mut out)?;
85+
} else {
86+
encode::trusted_header_signature(b"tagger", &tagger, &mut out)?;
87+
}
6988
}
7089

7190
if !self.message.iter().all(|b| *b == b'\n') {
@@ -87,10 +106,13 @@ impl crate::WriteTo for TagRef<'_> {
87106
(b"object".len() + 1 /* space */ + self.target().kind().len_in_hex() + 1 /* nl */
88107
+ b"type".len() + 1 /* space */ + self.target_kind.as_bytes().len() + 1 /* nl */
89108
+ b"tag".len() + 1 /* space */ + self.name.len() + 1 /* nl */
90-
+ self
91-
.tagger
92-
.as_ref()
93-
.map_or(0, |t| b"tagger".len() + 1 /* space */ + t.size() + 1 /* nl */)
109+
+ match (self.tagger, self.tagger_raw) {
110+
(Some(tagger), Some(raw)) => {
111+
b"tagger".len() + 1 /* space */ + signature_len(&tagger, raw) + 1 /* nl */
112+
}
113+
(None, None) => 0,
114+
_ => unreachable!("tagger raw data must be present if tagger exists"),
115+
}
94116
+ if self.message.iter().all(|b| *b == b'\n') { 0 } else { 1 /* nl */ } + self.message.len()
95117
+ self.pgp_signature.as_ref().map_or(0, |m| 1 /* nl */ + m.len())) as u64
96118
}

0 commit comments

Comments
 (0)