Unified · Split
| @@ -1,4 +1,5 @@ | |||
| 1 | use bstr::{BStr, ByteSlice}; | 1 | use bstr::{BStr, BString, ByteSlice}; |
| 2 | use std::ops::Range; | ||
| 2 | 3 | ||
| 3 | use crate::{Commit, CommitRef, TagRef}; | 4 | use 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))] | ||
| 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 | |||
| 24 | /// | 53 | /// |
| 25 | pub mod ref_iter; | 54 | pub mod ref_iter; |
| 26 | 55 | ||
| 27 | mod write; | 56 | mod write; |
| 28 | 57 | ||
| 58 | /// Lifecycle | ||
| 29 | impl<'a> CommitRef<'a> { | 59 | impl<'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 signed | 66 | /// Access |
| 36 | pub fn extract_signature( | 67 | impl<'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. |
| @@ -1,4 +1,5 @@ | |||
| 1 | use std::borrow::Cow; | 1 | use std::borrow::Cow; |
| 2 | use std::ops::Range; | ||
| 2 | 3 | ||
| 3 | use bstr::BStr; | 4 | use bstr::BStr; |
| 4 | use gix_hash::{oid, ObjectId}; | 5 | use gix_hash::{oid, ObjectId}; |
| @@ -9,6 +10,7 @@ use nom::{ · 4 unmodified lines | |||
| 9 | error::context, | 10 | error::context, |
| 10 | }; | 11 | }; |
| 11 | 12 | ||
| 13 | use crate::commit::SignedData; | ||
| 12 | use crate::{bstr::ByteSlice, commit::decode, parse, parse::NL, CommitRefIter}; | 14 | use 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 | ||
| 33 | impl<'a> CommitRefIter<'a> { | 36 | impl<'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 | ||
| 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 | } | ||
| 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 decoding | 76 | /// 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 instead | 270 | /// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead. |
| 242 | pub struct CommitRefIterRaw<'a> { | 271 | struct CommitRefIterRaw<'a> { |
| 243 | inner: CommitRefIter<'a>, | 272 | data: &'a [u8], |
| 273 | state: State, | ||
| 274 | offset: usize, | ||
| 244 | } | 275 | } |
| 245 | 276 | ||
| 246 | impl<'a> Iterator for CommitRefIterRaw<'a> { | 277 | impl<'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 it | 305 | /// A combination of a parsed [`Token`] as well as the range of bytes that were consumed to parse it. |
| 268 | pub struct RawToken<'a> { | 306 | struct RawToken<'a> { |
| 269 | /// The parsed token | 307 | /// The parsed token. |
| 270 | pub token: Token<'a>, | 308 | token: Token<'a>, |
| 271 | /// The raw slice the token was parsed from | 309 | 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]. |
| @@ -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 | } | ||
| @@ -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 | } |
| @@ -159,7 +159,6 @@ mod method { · 158 unmodified lines | |||
| 159 | } | 159 | } |
| 160 | } | 160 | } |
| 161 | 161 | ||
| 162 | mod extracts_signature; | ||
| 163 | mod from_bytes; | 162 | mod from_bytes; |
| 164 | mod iter; | 163 | mod iter; |
| 165 | mod message; | 164 | mod message; |