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 | 4 | use crate::{Commit, CommitRef, TagRef}; |
| 4 | 5 | |
| @@ -21,63 +22,49 @@ pub struct MessageRef<'a> { · 16 unmodified lines | ||
| 21 | 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 | 54 | pub mod ref_iter; |
| 26 | 55 | |
| 27 | 56 | mod write; |
| 28 | 57 | |
| 58 | +/// Lifecycle | |
| 29 | 59 | impl<'a> CommitRef<'a> { |
| 30 | 60 | /// Deserialize a commit from the given `data` bytes while avoiding most allocations. |
| 31 | 61 | pub fn from_bytes(data: &'a [u8]) -> Result<CommitRef<'a>, crate::decode::Error> { |
| 32 | 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 | |
| 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> { | |
| 81 | 68 | /// Return the `tree` fields hash digest. |
| 82 | 69 | pub fn tree(&self) -> gix_hash::ObjectId { |
| 83 | 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 | 80 | /// Returns a convenient iterator over all extra headers. |
| 94 | 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 | 85 | /// Return the author, with whitespace trimmed. |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | use std::borrow::Cow; |
| 2 | +use std::ops::Range; | |
| 2 | 3 | |
| 3 | 4 | use bstr::BStr; |
| 4 | 5 | use gix_hash::{oid, ObjectId}; |
| @@ -9,6 +10,7 @@ use nom::{ · 4 unmodified lines | ||
| 9 | 10 | error::context, |
| 10 | 11 | }; |
| 11 | 12 | |
| 13 | +use crate::commit::SignedData; | |
| 12 | 14 | use crate::{bstr::ByteSlice, commit::decode, parse, parse::NL, CommitRefIter}; |
| 13 | 15 | |
| 14 | 16 | #[derive(Copy, Clone)] |
| @@ -30,6 +32,7 @@ pub(crate) enum State { · 15 unmodified lines | ||
| 30 | 32 | Message, |
| 31 | 33 | } |
| 32 | 34 | |
| 35 | +/// Lifecycle | |
| 33 | 36 | impl<'a> CommitRefIter<'a> { |
| 34 | 37 | /// Create a commit iterator from data. |
| 35 | 38 | pub fn from_bytes(data: &'a [u8]) -> CommitRefIter<'a> { |
| @@ -38,10 +41,36 @@ impl<'a> CommitRefIter<'a> { · 2 unmodified lines | ||
| 38 | 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`] | |
| 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 }))) | |
| 45 | 74 | } |
| 46 | 75 | |
| 47 | 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 | |
| 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, | |
| 244 | 275 | } |
| 245 | 276 | |
| 246 | 277 | impl<'a> Iterator for CommitRefIterRaw<'a> { |
| 247 | 278 | type Item = Result<RawToken<'a>, crate::decode::Error>; |
| 248 | 279 | |
| 249 | 280 | fn next(&mut self) -> Option<Self::Item> { |
| 250 | − if self.inner.data.is_empty() { | |
| 281 | + if self.data.is_empty() { | |
| 251 | 282 | return None; |
| 252 | 283 | } |
| 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 | + })) | |
| 258 | 296 | } |
| 259 | 297 | Err(err) => { |
| 260 | − self.inner.data = &[]; | |
| 298 | + self.data = &[]; | |
| 261 | 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 | |
| 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>, | |
| 273 | 310 | } |
| 274 | 311 | |
| 275 | 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 | 205 | assert_eq!(iter.author().ok(), Some(signature(1592437401)), "it's not consuming"); |
| 206 | 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 | 162 | mod from_bytes; |
| 164 | 163 | mod iter; |
| 165 | 164 | mod message; |