History

refactor

Sebastian Thiel committed 3 years ago

Commit fb95ead1 parent 2f9c0dd5 files · +147 −132Browse files

Unified · Split

5 files
  1. gix-object/src/commit/mod.rs +35 −48
  2. gix-object/src/commit/ref_iter.rs +56 −19
  3. gix-object/tests/commit/extracts_signature.rs +0 −64
  4. gix-object/tests/commit/iter.rs +56 −0
  5. gix-object/tests/commit/mod.rs +0 −1
gix-object/src/commit/mod.rs+35−48
@@ -1,4 +1,5 @@
1−use bstr::{BStr, ByteSlice};
1+use bstr::{BStr, BString, ByteSlice};
2+use std::ops::Range;
23
34 use crate::{Commit, CommitRef, TagRef};
45
@@ -21,63 +22,49 @@ pub struct MessageRef<'a> { · 16 unmodified lines
2122 pub body: Option<&'a BStr>,
2223 }
2324
25+/// The raw commit data, parseable by [`CommitRef`] or [`Commit`], which was fed into a program to produce a signature.
26+///
27+/// See [`extract_signature()`](crate::CommitRefIter::signature()) for how to obtain it.
28+// TODO: implement `std::io::Read` to avoid allocations
29+#[derive(PartialEq, Eq, Debug, Hash, Clone)]
30+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31+pub struct SignedData<'a> {
32+ /// The raw commit data that includes the signature.
33+ data: &'a [u8],
34+ /// The byte range at which we find the signature. All but the signature is the data that was signed.
35+ signature_range: Range<usize>,
36+}
37+
38+impl SignedData<'_> {
39+ /// Convenience method to obtain a copy of the signed data.
40+ pub fn to_bstring(&self) -> BString {
41+ let mut buf = BString::from(&self.data[..self.signature_range.start]);
42+ buf.extend_from_slice(&self.data[self.signature_range.end..]);
43+ buf
44+ }
45+}
46+
47+impl From<SignedData<'_>> for BString {
48+ fn from(value: SignedData<'_>) -> Self {
49+ value.to_bstring()
50+ }
51+}
52+
2453 ///
2554 pub mod ref_iter;
2655
2756 mod write;
2857
58+/// Lifecycle
2959 impl<'a> CommitRef<'a> {
3060 /// Deserialize a commit from the given `data` bytes while avoiding most allocations.
3161 pub fn from_bytes(data: &'a [u8]) -> Result<CommitRef<'a>, crate::decode::Error> {
3262 decode::commit(data).map(|(_, t)| t).map_err(crate::decode::Error::from)
3363 }
64+}
3465
35− /// Extracts the PGP signature and the signed commit data, if it is signed
36− pub fn extract_signature(
37− data: &'a [u8],
38− signed_data: &mut Vec<u8>,
39− ) -> Result<Option<std::borrow::Cow<'a, BStr>>, crate::decode::Error> {
40− use crate::commit::ref_iter::Token;
41− let pgp_sig_header = gix_actor::bstr::BStr::new(b"gpgsig");
42−
43− signed_data.clear();
44−
45− let mut signature = None;
46− let out = signed_data;
47−
48− for token in crate::CommitRefIter::from_bytes(data).into_raw_iter() {
49− let token = token?;
50−
51− // We don't care what the tokens are, just that they are valid, except
52− // in the case of the signature, which we skip as it is the only part
53− // of the commit that is not signed, obviously
54− if let Token::ExtraHeader((hname, hval)) = &token.token {
55− if hname == &pgp_sig_header {
56− signature = Some(hval.clone());
57− continue;
58− }
59− }
60−
61− // There is only one exception to the outputs, which is that the message
62− // is always separated by an additional empty line from the rest of the
63− // data
64− if matches!(token.token, Token::Message(_)) {
65− // If we don't have a signature when we've reached the message,
66− // there is none and we can just short circuit
67− if signature.is_none() {
68− out.clear();
69− return Ok(None);
70− }
71−
72− out.push(b'\n');
73− }
74−
75− out.extend_from_slice(token.buffer);
76− }
77−
78− Ok(signature)
79− }
80−
66+/// Access
67+impl<'a> CommitRef<'a> {
8168 /// Return the `tree` fields hash digest.
8269 pub fn tree(&self) -> gix_hash::ObjectId {
8370 gix_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing")
@@ -92,7 +79,7 @@ impl<'a> CommitRef<'a> { · 8 unmodified lines
9279
9380 /// Returns a convenient iterator over all extra headers.
9481 pub fn extra_headers(&self) -> crate::commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> {
95− crate::commit::ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref())))
82+ ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref())))
9683 }
9784
9885 /// Return the author, with whitespace trimmed.
gix-object/src/commit/ref_iter.rs+56−19
@@ -1,4 +1,5 @@
11 use std::borrow::Cow;
2+use std::ops::Range;
23
34 use bstr::BStr;
45 use gix_hash::{oid, ObjectId};
@@ -9,6 +10,7 @@ use nom::{ · 4 unmodified lines
910 error::context,
1011 };
1112
13+use crate::commit::SignedData;
1214 use crate::{bstr::ByteSlice, commit::decode, parse, parse::NL, CommitRefIter};
1315
1416 #[derive(Copy, Clone)]
@@ -30,6 +32,7 @@ pub(crate) enum State { · 15 unmodified lines
3032 Message,
3133 }
3234
35+/// Lifecycle
3336 impl<'a> CommitRefIter<'a> {
3437 /// Create a commit iterator from data.
3538 pub fn from_bytes(data: &'a [u8]) -> CommitRefIter<'a> {
@@ -38,10 +41,36 @@ impl<'a> CommitRefIter<'a> { · 2 unmodified lines
3841 state: State::default(),
3942 }
4043 }
44+}
45+
46+/// Access
47+impl<'a> CommitRefIter<'a> {
48+ /// Parse `data` as commit and return its PGP signature, along with *all non-signature* data as [`SignedData`], or `None`
49+ /// if the commit isn't signed.
50+ ///
51+ /// This allows the caller to validate the signature by passing the signed data along with the signature back to the program
52+ /// that created it.
53+ pub fn signature(data: &'a [u8]) -> Result<Option<(Cow<'a, BStr>, SignedData<'a>)>, crate::decode::Error> {
54+ let mut signature_and_range = None;
55+
56+ let raw_tokens = CommitRefIterRaw {
57+ data,
58+ state: State::default(),
59+ offset: 0,
60+ };
61+ for token in raw_tokens {
62+ let token = token?;
63+ if let Token::ExtraHeader((name, value)) = &token.token {
64+ if *name == "gpgsig" {
65+ // keep track of the signature range alongside the signature data,
66+ // because all but the signature is the signed data.
67+ signature_and_range = Some((value.clone(), token.token_range));
68+ break;
69+ }
70+ }
71+ }
4172
42− /// Converts this iterator into a [`CommitRefIterRaw`]
43− pub fn into_raw_iter(self) -> CommitRefIterRaw<'a> {
44− CommitRefIterRaw { inner: self }
73+ Ok(signature_and_range.map(|(sig, signature_range)| (sig, SignedData { data, signature_range })))
4574 }
4675
4776 /// Returns the object id of this commits tree if it is the first function called and if there is no error in decoding
@@ -238,38 +267,46 @@ impl<'a> Iterator for CommitRefIter<'a> { · 190 unmodified lines
238267 }
239268 }
240269
241−/// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead
242−pub struct CommitRefIterRaw<'a> {
243− inner: CommitRefIter<'a>,
270+/// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead.
271+struct CommitRefIterRaw<'a> {
272+ data: &'a [u8],
273+ state: State,
274+ offset: usize,
244275 }
245276
246277 impl<'a> Iterator for CommitRefIterRaw<'a> {
247278 type Item = Result<RawToken<'a>, crate::decode::Error>;
248279
249280 fn next(&mut self) -> Option<Self::Item> {
250− if self.inner.data.is_empty() {
281+ if self.data.is_empty() {
251282 return None;
252283 }
253− match CommitRefIter::next_inner(self.inner.data, &mut self.inner.state) {
254− Ok((data, token)) => {
255− let buffer = &self.inner.data[..self.inner.data.len() - data.len()];
256− self.inner.data = data;
257− Some(Ok(RawToken { token, buffer }))
284+ match CommitRefIter::next_inner(self.data, &mut self.state) {
285+ Ok((remaining, token)) => {
286+ let consumed = self.data.len() - remaining.len();
287+ let start = self.offset;
288+ let end = start + consumed;
289+ self.offset = end;
290+
291+ self.data = remaining;
292+ Some(Ok(RawToken {
293+ token,
294+ token_range: start..end,
295+ }))
258296 }
259297 Err(err) => {
260− self.inner.data = &[];
298+ self.data = &[];
261299 Some(Err(err))
262300 }
263301 }
264302 }
265303 }
266304
267−/// A combination of a parsed [`Token`] as well as the raw buffer that produced it
268−pub struct RawToken<'a> {
269− /// The parsed token
270− pub token: Token<'a>,
271− /// The raw slice the token was parsed from
272− pub buffer: &'a [u8],
305+/// A combination of a parsed [`Token`] as well as the range of bytes that were consumed to parse it.
306+struct RawToken<'a> {
307+ /// The parsed token.
308+ token: Token<'a>,
309+ token_range: Range<usize>,
273310 }
274311
275312 /// A token returned by the [commit iterator][CommitRefIter].
gix-object/tests/commit/extracts_signature.rs+0−64
@@ -1,64 +0,0 @@
1−use gix_object::{bstr::ByteSlice, CommitRef};
2−
3−use crate::{
4− commit::{OTHER_SIGNATURE, SIGNATURE},
5− fixture_name,
6−};
7−
8−fn extract_signed_helper(
9− fixture: &str,
10− signature: &[u8],
11− signature_lines: std::ops::RangeInclusive<usize>,
12−) -> crate::Result {
13− let fixture_data = fixture_name("commit", fixture);
14− let mut signed_data = Vec::new();
15− {
16− let sig = CommitRef::extract_signature(&fixture_data, &mut signed_data)?.unwrap();
17− assert_eq!(sig.to_str(), signature.to_str());
18− }
19−
20− let ends_with_newline = fixture_data[fixture_data.len() - 1] == b'\n';
21− let mut expected = Vec::new();
22− for (i, line) in String::from_utf8(fixture_data).unwrap().lines().enumerate() {
23− if signature_lines.contains(&i) {
24− if *signature_lines.end() == i {
25− expected.push(b'\n');
26− }
27− continue;
28− }
29− expected.extend_from_slice(line.as_bytes());
30− expected.push(b'\n');
31− }
32−
33− if !ends_with_newline {
34− expected.pop();
35− }
36−
37− assert_eq!(signed_data.to_str(), expected.to_str());
38− Ok(())
39−}
40−
41−#[test]
42−fn extract_sig_singleline() -> crate::Result {
43− extract_signed_helper("signed-singleline.txt", b"magic:signature", 4..=4)
44−}
45−
46−#[test]
47−fn extract_sig_signed() -> crate::Result {
48− extract_signed_helper("signed.txt", b"-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCAAdFiEEdjYp/sh4j8NRKLX27gKdHl60AwAFAl7p9tgACgkQ7gKdHl60\nAwBpegf+KQciv9AOIN7+yPmowecGxBnSfpKWTDzFxnyGR8dq63SpWT8WEKG5mf3a\nG6iUqpsDWaMHlzihaMKRvgRpZxFRbjnNPFBj6F4RRqfE+5R7k6DRSLUV5PqnsdSH\nuccfIDWi1imhsm7AaP5trwl1t+83U2JhHqPcPVFLMODYwWeO6NLR/JCzGSTQRa8t\nRgaVMKI19O/fge5OT5Ua8D47VKEhsJX0LfmkP5RfZQ8JJvNd40TupqKRdlv0sAzP\nya7NXkSHXCavHNR6kA+KpWxn900UoGK8/IDlwU6MeOkpPVawb3NFMqnc7KJDaC2p\nSMzpuEG8LTrCx2YSpHNLqHyzvQ1CZA==\n=5ITV\n-----END PGP SIGNATURE-----", 4..=14)
49−}
50−
51−#[test]
52−fn extract_sig_with_encoding() -> crate::Result {
53− extract_signed_helper("signed-with-encoding.txt", SIGNATURE, 5..=15)
54−}
55−
56−#[test]
57−fn extract_sig_with_msg_footer() -> crate::Result {
58− extract_signed_helper("message-with-footer.txt", b"-----BEGIN PGP SIGNATURE-----\n\niHUEABYIAB0WIQSuZwcGWSQItmusNgR5URpSUCnwXQUCYT7xpAAKCRB5URpSUCnw\nXWB3AP9q323HlxnI8MyqszNOeYDwa7Y3yEZaUM2y/IRjz+z4YQEAq0yr1Syt3mrK\nOSFCqL2vDm3uStP+vF31f6FnzayhNg0=\n=Mhpp\n-----END PGP SIGNATURE-----", 4..=10)
59−}
60−
61−#[test]
62−fn extract_sig_whitespace() -> crate::Result {
63− extract_signed_helper("signed-whitespace.txt", OTHER_SIGNATURE, 5..=15)
64−}
gix-object/tests/commit/iter.rs+56−0
@@ -205,4 +205,60 @@ mod method { · 204 unmodified lines
205205 assert_eq!(iter.author().ok(), Some(signature(1592437401)), "it's not consuming");
206206 Ok(())
207207 }
208+
209+ mod signature {
210+ use bstr::{BStr, BString, ByteSlice};
211+ use gix_object::CommitRefIter;
212+
213+ use crate::{
214+ commit::{OTHER_SIGNATURE, SIGNATURE},
215+ fixture_name,
216+ };
217+
218+ fn validate<'a>(
219+ fixture: &str,
220+ expected_signature: impl Into<&'a BStr>,
221+ signature_lines: std::ops::RangeInclusive<usize>,
222+ ) -> crate::Result {
223+ let expected_signature = expected_signature.into();
224+ let fixture_data = fixture_name("commit", fixture);
225+
226+ let (actual_signature, actual_signed_data) = CommitRefIter::signature(&fixture_data)?.expect("sig present");
227+ assert_eq!(actual_signature, expected_signature);
228+
229+ let expected_signed_data: BString = fixture_data
230+ .lines_with_terminator()
231+ .enumerate()
232+ .filter_map(|(i, line)| (!signature_lines.contains(&i)).then_some(line))
233+ .collect();
234+
235+ assert_eq!(actual_signed_data.to_bstring(), expected_signed_data);
236+ Ok(())
237+ }
238+
239+ #[test]
240+ fn single_line() -> crate::Result {
241+ validate("signed-singleline.txt", b"magic:signature", 4..=4)
242+ }
243+
244+ #[test]
245+ fn signed() -> crate::Result {
246+ validate("signed.txt", b"-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCAAdFiEEdjYp/sh4j8NRKLX27gKdHl60AwAFAl7p9tgACgkQ7gKdHl60\nAwBpegf+KQciv9AOIN7+yPmowecGxBnSfpKWTDzFxnyGR8dq63SpWT8WEKG5mf3a\nG6iUqpsDWaMHlzihaMKRvgRpZxFRbjnNPFBj6F4RRqfE+5R7k6DRSLUV5PqnsdSH\nuccfIDWi1imhsm7AaP5trwl1t+83U2JhHqPcPVFLMODYwWeO6NLR/JCzGSTQRa8t\nRgaVMKI19O/fge5OT5Ua8D47VKEhsJX0LfmkP5RfZQ8JJvNd40TupqKRdlv0sAzP\nya7NXkSHXCavHNR6kA+KpWxn900UoGK8/IDlwU6MeOkpPVawb3NFMqnc7KJDaC2p\nSMzpuEG8LTrCx2YSpHNLqHyzvQ1CZA==\n=5ITV\n-----END PGP SIGNATURE-----", 4..=14)
247+ }
248+
249+ #[test]
250+ fn with_encoding() -> crate::Result {
251+ validate("signed-with-encoding.txt", SIGNATURE, 5..=15)
252+ }
253+
254+ #[test]
255+ fn msg_footer() -> crate::Result {
256+ validate("message-with-footer.txt", b"-----BEGIN PGP SIGNATURE-----\n\niHUEABYIAB0WIQSuZwcGWSQItmusNgR5URpSUCnwXQUCYT7xpAAKCRB5URpSUCnw\nXWB3AP9q323HlxnI8MyqszNOeYDwa7Y3yEZaUM2y/IRjz+z4YQEAq0yr1Syt3mrK\nOSFCqL2vDm3uStP+vF31f6FnzayhNg0=\n=Mhpp\n-----END PGP SIGNATURE-----", 4..=10)
257+ }
258+
259+ #[test]
260+ fn whitespace() -> crate::Result {
261+ validate("signed-whitespace.txt", OTHER_SIGNATURE, 5..=15)
262+ }
263+ }
208264 }
gix-object/tests/commit/mod.rs+0−1
@@ -159,7 +159,6 @@ mod method { · 158 unmodified lines
159159 }
160160 }
161161
162−mod extracts_signature;
163162 mod from_bytes;
164163 mod iter;
165164 mod message;