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 @@
1use bstr::{BStr, ByteSlice};1use bstr::{BStr, BString, ByteSlice};
2use std::ops::Range;
2 3
3use crate::{Commit, CommitRef, TagRef};4use crate::{Commit, CommitRef, TagRef};
4 5
@@ -21,63 +22,49 @@ pub struct MessageRef<'a> { · 16 unmodified lines
21 pub body: Option<&'a BStr>,22 pub body: Option<&'a BStr>,
22}23}
23 24
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))]
31pub 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
38impl 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
47impl From<SignedData<'_>> for BString {
48 fn from(value: SignedData<'_>) -> Self {
49 value.to_bstring()
50 }
51}
52
24///53///
25pub mod ref_iter;54pub mod ref_iter;
26 55
27mod write;56mod write;
28 57
58/// Lifecycle
29impl<'a> CommitRef<'a> {59impl<'a> CommitRef<'a> {
30 /// Deserialize a commit from the given `data` bytes while avoiding most allocations.60 /// Deserialize a commit from the given `data` bytes while avoiding most allocations.
31 pub fn from_bytes(data: &'a [u8]) -> Result<CommitRef<'a>, crate::decode::Error> {61 pub fn from_bytes(data: &'a [u8]) -> Result<CommitRef<'a>, crate::decode::Error> {
32 decode::commit(data).map(|(_, t)| t).map_err(crate::decode::Error::from)62 decode::commit(data).map(|(_, t)| t).map_err(crate::decode::Error::from)
33 }63 }
64}
34 65
35 /// Extracts the PGP signature and the signed commit data, if it is signed66/// Access
36 pub fn extract_signature(67impl<'a> CommitRef<'a> {
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
81 /// Return the `tree` fields hash digest.68 /// Return the `tree` fields hash digest.
82 pub fn tree(&self) -> gix_hash::ObjectId {69 pub fn tree(&self) -> gix_hash::ObjectId {
83 gix_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing")70 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
92 79
93 /// Returns a convenient iterator over all extra headers.80 /// Returns a convenient iterator over all extra headers.
94 pub fn extra_headers(&self) -> crate::commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> {81 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())))
96 }83 }
97 84
98 /// Return the author, with whitespace trimmed.85 /// Return the author, with whitespace trimmed.
gix-object/src/commit/ref_iter.rs+56−19
@@ -1,4 +1,5 @@
1use std::borrow::Cow;1use std::borrow::Cow;
2use std::ops::Range;
2 3
3use bstr::BStr;4use bstr::BStr;
4use gix_hash::{oid, ObjectId};5use gix_hash::{oid, ObjectId};
@@ -9,6 +10,7 @@ use nom::{ · 4 unmodified lines
9 error::context,10 error::context,
10};11};
11 12
13use crate::commit::SignedData;
12use crate::{bstr::ByteSlice, commit::decode, parse, parse::NL, CommitRefIter};14use crate::{bstr::ByteSlice, commit::decode, parse, parse::NL, CommitRefIter};
13 15
14#[derive(Copy, Clone)]16#[derive(Copy, Clone)]
@@ -30,6 +32,7 @@ pub(crate) enum State { · 15 unmodified lines
30 Message,32 Message,
31}33}
32 34
35/// Lifecycle
33impl<'a> CommitRefIter<'a> {36impl<'a> CommitRefIter<'a> {
34 /// Create a commit iterator from data.37 /// Create a commit iterator from data.
35 pub fn from_bytes(data: &'a [u8]) -> CommitRefIter<'a> {38 pub fn from_bytes(data: &'a [u8]) -> CommitRefIter<'a> {
@@ -38,10 +41,36 @@ impl<'a> CommitRefIter<'a> { · 2 unmodified lines
38 state: State::default(),41 state: State::default(),
39 }42 }
40 }43 }
44}
45
46/// Access
47impl<'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 }
41 72
42 /// Converts this iterator into a [`CommitRefIterRaw`]73 Ok(signature_and_range.map(|(sig, signature_range)| (sig, SignedData { data, signature_range })))
43 pub fn into_raw_iter(self) -> CommitRefIterRaw<'a> {
44 CommitRefIterRaw { inner: self }
45 }74 }
46 75
47 /// Returns the object id of this commits tree if it is the first function called and if there is no error in decoding76 /// 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
238 }267 }
239}268}
240 269
241/// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead270/// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead.
242pub struct CommitRefIterRaw<'a> {271struct CommitRefIterRaw<'a> {
243 inner: CommitRefIter<'a>,272 data: &'a [u8],
273 state: State,
274 offset: usize,
244}275}
245 276
246impl<'a> Iterator for CommitRefIterRaw<'a> {277impl<'a> Iterator for CommitRefIterRaw<'a> {
247 type Item = Result<RawToken<'a>, crate::decode::Error>;278 type Item = Result<RawToken<'a>, crate::decode::Error>;
248 279
249 fn next(&mut self) -> Option<Self::Item> {280 fn next(&mut self) -> Option<Self::Item> {
250 if self.inner.data.is_empty() {281 if self.data.is_empty() {
251 return None;282 return None;
252 }283 }
253 match CommitRefIter::next_inner(self.inner.data, &mut self.inner.state) {284 match CommitRefIter::next_inner(self.data, &mut self.state) {
254 Ok((data, token)) => {285 Ok((remaining, token)) => {
255 let buffer = &self.inner.data[..self.inner.data.len() - data.len()];286 let consumed = self.data.len() - remaining.len();
256 self.inner.data = data;287 let start = self.offset;
257 Some(Ok(RawToken { token, buffer }))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 }))
258 }296 }
259 Err(err) => {297 Err(err) => {
260 self.inner.data = &[];298 self.data = &[];
261 Some(Err(err))299 Some(Err(err))
262 }300 }
263 }301 }
264 }302 }
265}303}
266 304
267/// A combination of a parsed [`Token`] as well as the raw buffer that produced it305/// A combination of a parsed [`Token`] as well as the range of bytes that were consumed to parse it.
268pub struct RawToken<'a> {306struct RawToken<'a> {
269 /// The parsed token307 /// The parsed token.
270 pub token: Token<'a>,308 token: Token<'a>,
271 /// The raw slice the token was parsed from309 token_range: Range<usize>,
272 pub buffer: &'a [u8],
273}310}
274 311
275/// A token returned by the [commit iterator][CommitRefIter].312/// A token returned by the [commit iterator][CommitRefIter].
gix-object/tests/commit/extracts_signature.rs+0−64
@@ -1,64 +0,0 @@
1use gix_object::{bstr::ByteSlice, CommitRef};
2
3use crate::{
4 commit::{OTHER_SIGNATURE, SIGNATURE},
5 fixture_name,
6};
7
8fn 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]
42fn extract_sig_singleline() -> crate::Result {
43 extract_signed_helper("signed-singleline.txt", b"magic:signature", 4..=4)
44}
45
46#[test]
47fn 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]
52fn extract_sig_with_encoding() -> crate::Result {
53 extract_signed_helper("signed-with-encoding.txt", SIGNATURE, 5..=15)
54}
55
56#[test]
57fn 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]
62fn 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
205 assert_eq!(iter.author().ok(), Some(signature(1592437401)), "it's not consuming");205 assert_eq!(iter.author().ok(), Some(signature(1592437401)), "it's not consuming");
206 Ok(())206 Ok(())
207 }207 }
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 }
208}264}
gix-object/tests/commit/mod.rs+0−1
@@ -159,7 +159,6 @@ mod method { · 158 unmodified lines
159 }159 }
160}160}
161 161
162mod extracts_signature;
163mod from_bytes;162mod from_bytes;
164mod iter;163mod iter;
165mod message;164mod message;