This categorizes them semantically without using the overloaded names of owning and borrowing, which mean different things in Rust.
Showing the changes to git-object/src/immutable (8 of 34 files). Show all
Unified · Split
| @@ -0,0 +1,16 @@ | |||
| 1 | use std::convert::Infallible; | ||
| 2 | |||
| 3 | /// A chunk of any [`data`][Blob::data]. | ||
| 4 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 5 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 6 | pub struct Blob<'a> { | ||
| 7 | /// The bytes themselves. | ||
| 8 | pub data: &'a [u8], | ||
| 9 | } | ||
| 10 | |||
| 11 | impl<'a> Blob<'a> { | ||
| 12 | /// Instantiate a `Blob` from the given `data`, which is used as-is. | ||
| 13 | pub fn from_bytes(data: &[u8]) -> Result<Blob<'_>, Infallible> { | ||
| 14 | Ok(Blob { data }) | ||
| 15 | } | ||
| 16 | } | ||
| @@ -0,0 +1,109 @@ | |||
| 1 | use super::Error; | ||
| 2 | use crate::{ | ||
| 3 | commit, | ||
| 4 | immutable::{parse, parse::NL, Signature}, | ||
| 5 | BStr, ByteSlice, | ||
| 6 | }; | ||
| 7 | use nom::{ | ||
| 8 | branch::alt, | ||
| 9 | bytes::{complete::is_not, complete::tag}, | ||
| 10 | combinator::{all_consuming, opt}, | ||
| 11 | multi::many0, | ||
| 12 | IResult, | ||
| 13 | }; | ||
| 14 | use smallvec::SmallVec; | ||
| 15 | use std::borrow::Cow; | ||
| 16 | |||
| 17 | /// A git commit parsed using [`from_bytes()`][Commit::from_bytes()]. | ||
| 18 | /// | ||
| 19 | /// A commit encapsulates information about a point in time at which the state of the repository is recorded, usually after a | ||
| 20 | /// change which is documented in the commit `message`. | ||
| 21 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 22 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 23 | pub struct Commit<'a> { | ||
| 24 | /// HEX hash of tree object we point to. Usually 40 bytes long. | ||
| 25 | /// | ||
| 26 | /// Use [`tree()`][Commit::tree()] to obtain a decoded version of it. | ||
| 27 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 28 | pub tree: &'a BStr, | ||
| 29 | /// HEX hash of each parent commit. Empty for first commit in repository. | ||
| 30 | pub parents: SmallVec<[&'a BStr; 1]>, | ||
| 31 | /// Who wrote this commit. | ||
| 32 | pub author: Signature<'a>, | ||
| 33 | /// Who committed this commit. | ||
| 34 | /// | ||
| 35 | /// This may be different from the `author` in case the author couldn't write to the repository themselves and | ||
| 36 | /// is commonly encountered with contributed commits. | ||
| 37 | pub committer: Signature<'a>, | ||
| 38 | /// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493). | ||
| 39 | pub encoding: Option<&'a BStr>, | ||
| 40 | /// The commit message documenting the change. | ||
| 41 | pub message: &'a BStr, | ||
| 42 | /// Extra header fields, in order of them being encountered, made accessible with the iterator returned by [`extra_headers()`][Commit::extra_headers()]. | ||
| 43 | pub extra_headers: Vec<(&'a BStr, Cow<'a, BStr>)>, | ||
| 44 | } | ||
| 45 | |||
| 46 | impl<'a> Commit<'a> { | ||
| 47 | /// Deserialize a commit from the given `data` bytes while avoiding most allocations. | ||
| 48 | pub fn from_bytes(data: &'a [u8]) -> Result<Commit<'a>, Error> { | ||
| 49 | parse(data).map(|(_, t)| t).map_err(Error::from) | ||
| 50 | } | ||
| 51 | /// Return the `tree` fields hash digest. | ||
| 52 | pub fn tree(&self) -> git_hash::ObjectId { | ||
| 53 | git_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing") | ||
| 54 | } | ||
| 55 | |||
| 56 | /// Returns an iterator of parent object ids | ||
| 57 | pub fn parents(&self) -> impl Iterator<Item = git_hash::ObjectId> + '_ { | ||
| 58 | self.parents | ||
| 59 | .iter() | ||
| 60 | .map(|hex_hash| git_hash::ObjectId::from_hex(hex_hash).expect("prior validation of hashes during parsing")) | ||
| 61 | } | ||
| 62 | |||
| 63 | /// Returns a convenient iterator over all extra headers. | ||
| 64 | pub fn extra_headers(&self) -> commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> { | ||
| 65 | commit::ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref()))) | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | fn parse_message(i: &[u8]) -> IResult<&[u8], &BStr, Error> { | ||
| 70 | if i.is_empty() { | ||
| 71 | // newline + [message] | ||
| 72 | return Err(nom::Err::Error(Error::NomDetail(i.into(), "commit message is missing"))); | ||
| 73 | } | ||
| 74 | let (i, _) = tag(NL)(i).map_err(Error::context("a newline separates headers from the message"))?; | ||
| 75 | debug_assert!(!i.is_empty()); | ||
| 76 | Ok((&[], &i.as_bstr())) | ||
| 77 | } | ||
| 78 | |||
| 79 | fn parse(i: &[u8]) -> IResult<&[u8], Commit<'_>, Error> { | ||
| 80 | let (i, tree) = | ||
| 81 | parse::header_field(i, b"tree", parse::hex_sha1).map_err(Error::context("tree <40 lowercase hex char>"))?; | ||
| 82 | let (i, parents) = many0(|i| parse::header_field(i, b"parent", parse::hex_sha1))(i) | ||
| 83 | .map_err(Error::context("zero or more 'parent <40 lowercase hex char>'"))?; | ||
| 84 | let (i, author) = | ||
| 85 | parse::header_field(i, b"author", parse::signature).map_err(Error::context("author <signature>"))?; | ||
| 86 | let (i, committer) = | ||
| 87 | parse::header_field(i, b"committer", parse::signature).map_err(Error::context("committer <signature>"))?; | ||
| 88 | let (i, encoding) = | ||
| 89 | opt(|i| parse::header_field(i, b"encoding", is_not(NL)))(i).map_err(Error::context("encoding <encoding>"))?; | ||
| 90 | let (i, extra_headers) = many0(alt(( | ||
| 91 | |i| parse::any_header_field_multi_line(i).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Owned(o)))), | ||
| 92 | |i| parse::any_header_field(i, is_not(NL)).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr())))), | ||
| 93 | )))(i) | ||
| 94 | .map_err(Error::context("<field> <single-line|multi-line>"))?; | ||
| 95 | let (i, message) = all_consuming(parse_message)(i)?; | ||
| 96 | |||
| 97 | Ok(( | ||
| 98 | i, | ||
| 99 | Commit { | ||
| 100 | tree, | ||
| 101 | parents: SmallVec::from(parents), | ||
| 102 | author, | ||
| 103 | committer, | ||
| 104 | encoding: encoding.map(ByteSlice::as_bstr), | ||
| 105 | message, | ||
| 106 | extra_headers, | ||
| 107 | }, | ||
| 108 | )) | ||
| 109 | } | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | //! Immutable objects are read-only structures referencing most data from [a byte slice][Object::from_bytes()]. | ||
| 2 | //! | ||
| 3 | //! Immutable objects are expected to be deserialized from bytes that acts as backing store, and they | ||
| 4 | //! cannot be mutated or serialized. Instead, one will [convert][Object::into_mutable()] them into their [`mutable`][crate::mutable] counterparts | ||
| 5 | //! which support mutation and serialization. | ||
| 6 | mod commit; | ||
| 7 | pub use commit::Commit; | ||
| 8 | |||
| 9 | mod tag; | ||
| 10 | pub use tag::Tag; | ||
| 11 | |||
| 12 | /// | ||
| 13 | pub mod tree; | ||
| 14 | pub use tree::Tree; | ||
| 15 | |||
| 16 | mod blob; | ||
| 17 | pub use blob::Blob; | ||
| 18 | |||
| 19 | mod object; | ||
| 20 | pub use object::{Error, Object, Signature}; | ||
| 21 | |||
| 22 | mod parse; | ||
| @@ -0,0 +1,184 @@ | |||
| 1 | use bstr::BStr; | ||
| 2 | |||
| 3 | use crate::{ | ||
| 4 | immutable, | ||
| 5 | immutable::{parse, Blob, Commit, Tag, Tree}, | ||
| 6 | Kind, Time, | ||
| 7 | }; | ||
| 8 | |||
| 9 | mod error; | ||
| 10 | pub use error::Error; | ||
| 11 | |||
| 12 | /// A signature is created by an actor at a certain time. | ||
| 13 | /// | ||
| 14 | /// Note that this is not a cryptographical signature. | ||
| 15 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 16 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 17 | pub struct Signature<'a> { | ||
| 18 | /// The actor's name. | ||
| 19 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 20 | pub name: &'a BStr, | ||
| 21 | /// The actor's email. | ||
| 22 | pub email: &'a BStr, | ||
| 23 | /// The time stamp at which the signature was performed. | ||
| 24 | pub time: Time, | ||
| 25 | } | ||
| 26 | |||
| 27 | impl<'a> Signature<'a> { | ||
| 28 | /// Deserialize a signature from the given `data`. | ||
| 29 | pub fn from_bytes(data: &'a [u8]) -> Result<Signature<'a>, Error> { | ||
| 30 | parse::signature(data).map(|(_, t)| t).map_err(Error::from) | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | /// An immutable object representing [`Trees`][Tree], [`Blobs`][Blob], [`Commits`][Commit], or [`Tags`][Tag]. | ||
| 35 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 36 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 37 | #[allow(missing_docs)] | ||
| 38 | pub enum Object<'a> { | ||
| 39 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 40 | Tree(Tree<'a>), | ||
| 41 | Blob(Blob<'a>), | ||
| 42 | Commit(Commit<'a>), | ||
| 43 | Tag(Tag<'a>), | ||
| 44 | } | ||
| 45 | |||
| 46 | impl<'a> Object<'a> { | ||
| 47 | /// Deserialize an object of `kind` from the given `data`. | ||
| 48 | pub fn from_bytes(kind: Kind, data: &'a [u8]) -> Result<Object<'a>, Error> { | ||
| 49 | Ok(match kind { | ||
| 50 | Kind::Tree => Object::Tree(Tree::from_bytes(data)?), | ||
| 51 | Kind::Blob => Object::Blob(Blob { data }), | ||
| 52 | Kind::Commit => Object::Commit(Commit::from_bytes(data)?), | ||
| 53 | Kind::Tag => Object::Tag(Tag::from_bytes(data)?), | ||
| 54 | }) | ||
| 55 | } | ||
| 56 | |||
| 57 | /// Convert the immutable object into a mutable version, consuming the source in the process. | ||
| 58 | /// | ||
| 59 | /// Note that this is an expensive operation. | ||
| 60 | pub fn into_mutable(self) -> crate::mutable::Object { | ||
| 61 | self.into() | ||
| 62 | } | ||
| 63 | |||
| 64 | /// Convert this immutable object into its mutable counterpart. | ||
| 65 | /// | ||
| 66 | /// Note that this is an expensive operation. | ||
| 67 | pub fn to_mutable(&self) -> crate::mutable::Object { | ||
| 68 | self.clone().into() | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | /// Convenient access to contained objects. | ||
| 73 | impl<'a> Object<'a> { | ||
| 74 | /// Interpret this object as blob. | ||
| 75 | pub fn as_blob(&self) -> Option<&immutable::Blob<'_>> { | ||
| 76 | match self { | ||
| 77 | Object::Blob(v) => Some(v), | ||
| 78 | _ => None, | ||
| 79 | } | ||
| 80 | } | ||
| 81 | /// Interpret this object as commit. | ||
| 82 | pub fn as_commit(&self) -> Option<&immutable::Commit<'a>> { | ||
| 83 | match self { | ||
| 84 | Object::Commit(v) => Some(v), | ||
| 85 | _ => None, | ||
| 86 | } | ||
| 87 | } | ||
| 88 | /// Interpret this object as tree. | ||
| 89 | pub fn as_tree(&self) -> Option<&immutable::Tree<'_>> { | ||
| 90 | match self { | ||
| 91 | Object::Tree(v) => Some(v), | ||
| 92 | _ => None, | ||
| 93 | } | ||
| 94 | } | ||
| 95 | /// Interpret this object as tag. | ||
| 96 | pub fn as_tag(&self) -> Option<&immutable::Tag<'_>> { | ||
| 97 | match self { | ||
| 98 | Object::Tag(v) => Some(v), | ||
| 99 | _ => None, | ||
| 100 | } | ||
| 101 | } | ||
| 102 | /// Return the kind of object. | ||
| 103 | pub fn kind(&self) -> Kind { | ||
| 104 | match self { | ||
| 105 | Object::Tree(_) => Kind::Tree, | ||
| 106 | Object::Blob(_) => Kind::Blob, | ||
| 107 | Object::Commit(_) => Kind::Commit, | ||
| 108 | Object::Tag(_) => Kind::Tag, | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | mod convert { | ||
| 114 | use crate::immutable::{Blob, Commit, Object, Tag, Tree}; | ||
| 115 | use std::convert::TryFrom; | ||
| 116 | |||
| 117 | impl<'a> From<Tag<'a>> for Object<'a> { | ||
| 118 | fn from(v: Tag<'a>) -> Self { | ||
| 119 | Object::Tag(v) | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | impl<'a> From<Commit<'a>> for Object<'a> { | ||
| 124 | fn from(v: Commit<'a>) -> Self { | ||
| 125 | Object::Commit(v) | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | impl<'a> From<Tree<'a>> for Object<'a> { | ||
| 130 | fn from(v: Tree<'a>) -> Self { | ||
| 131 | Object::Tree(v) | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | impl<'a> From<Blob<'a>> for Object<'a> { | ||
| 136 | fn from(v: Blob<'a>) -> Self { | ||
| 137 | Object::Blob(v) | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | impl<'a> TryFrom<Object<'a>> for Tag<'a> { | ||
| 142 | type Error = Object<'a>; | ||
| 143 | |||
| 144 | fn try_from(value: Object<'a>) -> Result<Self, Self::Error> { | ||
| 145 | Ok(match value { | ||
| 146 | Object::Tag(v) => v, | ||
| 147 | _ => return Err(value), | ||
| 148 | }) | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | impl<'a> TryFrom<Object<'a>> for Commit<'a> { | ||
| 153 | type Error = Object<'a>; | ||
| 154 | |||
| 155 | fn try_from(value: Object<'a>) -> Result<Self, Self::Error> { | ||
| 156 | Ok(match value { | ||
| 157 | Object::Commit(v) => v, | ||
| 158 | _ => return Err(value), | ||
| 159 | }) | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | impl<'a> TryFrom<Object<'a>> for Tree<'a> { | ||
| 164 | type Error = Object<'a>; | ||
| 165 | |||
| 166 | fn try_from(value: Object<'a>) -> Result<Self, Self::Error> { | ||
| 167 | Ok(match value { | ||
| 168 | Object::Tree(v) => v, | ||
| 169 | _ => return Err(value), | ||
| 170 | }) | ||
| 171 | } | ||
| 172 | } | ||
| 173 | |||
| 174 | impl<'a> TryFrom<Object<'a>> for Blob<'a> { | ||
| 175 | type Error = Object<'a>; | ||
| 176 | |||
| 177 | fn try_from(value: Object<'a>) -> Result<Self, Self::Error> { | ||
| 178 | Ok(match value { | ||
| 179 | Object::Blob(v) => v, | ||
| 180 | _ => return Err(value), | ||
| 181 | }) | ||
| 182 | } | ||
| 183 | } | ||
| 184 | } | ||
| @@ -0,0 +1,59 @@ | |||
| 1 | use nom::error::ParseError; | ||
| 2 | use quick_error::quick_error; | ||
| 3 | quick_error! { | ||
| 4 | /// An error returned by various [`Commit`][crate::immutable::Commit] and [`Signature`][crate::immutable::Signature] methods. | ||
| 5 | #[derive(Debug)] | ||
| 6 | #[allow(missing_docs)] | ||
| 7 | pub enum Error { | ||
| 8 | ParseIntegerError(msg: &'static str, number: crate::BString, err: btoi::ParseIntegerError) { | ||
| 9 | display("{}: {:?}", msg, number) | ||
| 10 | source(err) | ||
| 11 | } | ||
| 12 | Nom(err_msg: String) { | ||
| 13 | display("{}", err_msg) | ||
| 14 | } | ||
| 15 | NomDetail(input: crate::BString, msg: &'static str) { | ||
| 16 | display("{}: '{}' could not be parsed", msg, input) | ||
| 17 | } | ||
| 18 | ParseKindError(err: crate::types::Error) { | ||
| 19 | display("{}", err) | ||
| 20 | source(err) | ||
| 21 | } | ||
| 22 | ObjectKind(err: crate::Error) { | ||
| 23 | from() | ||
| 24 | source(err) | ||
| 25 | } | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | impl Error { | ||
| 30 | fn set_parse_context(mut self, ctx: &'static str) -> Self { | ||
| 31 | if let Error::NomDetail(_, ref mut message) = self { | ||
| 32 | *message = ctx | ||
| 33 | } | ||
| 34 | self | ||
| 35 | } | ||
| 36 | |||
| 37 | pub(crate) fn context(msg: &'static str) -> impl Fn(nom::Err<Self>) -> nom::Err<Self> { | ||
| 38 | move |e: nom::Err<Self>| e.map(|e| e.set_parse_context(msg)) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | impl ParseError<&[u8]> for Error { | ||
| 43 | fn from_error_kind(input: &[u8], _kind: nom::error::ErrorKind) -> Self { | ||
| 44 | Error::NomDetail(input.into(), "parse error") | ||
| 45 | } | ||
| 46 | |||
| 47 | fn append(_: &[u8], _: nom::error::ErrorKind, other: Self) -> Self { | ||
| 48 | other | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | impl From<nom::Err<Error>> for Error { | ||
| 53 | fn from(e: nom::Err<Error>) -> Self { | ||
| 54 | match e { | ||
| 55 | nom::Err::Error(err) | nom::Err::Failure(err) => Error::Nom(err.to_string()), | ||
| 56 | nom::Err::Incomplete(_) => unreachable!("we do not implement streaming parsers"), | ||
| 57 | } | ||
| 58 | } | ||
| 59 | } | ||
| @@ -0,0 +1,171 @@ | |||
| 1 | use crate::{ | ||
| 2 | immutable::{Error, Signature}, | ||
| 3 | ByteSlice, Sign, Time, | ||
| 4 | }; | ||
| 5 | use bstr::{BStr, BString, ByteVec}; | ||
| 6 | use btoi::btoi; | ||
| 7 | use nom::{ | ||
| 8 | branch::alt, | ||
| 9 | bytes::complete::{is_not, tag, take, take_until, take_while_m_n}, | ||
| 10 | character::is_digit, | ||
| 11 | combinator::{peek, recognize}, | ||
| 12 | multi::many1_count, | ||
| 13 | sequence::{preceded, terminated, tuple}, | ||
| 14 | IResult, | ||
| 15 | }; | ||
| 16 | |||
| 17 | pub(crate) const NL: &[u8] = b"\n"; | ||
| 18 | pub(crate) const SPACE: &[u8] = b" "; | ||
| 19 | pub(crate) const SPACE_OR_NL: &[u8] = b" \n"; | ||
| 20 | |||
| 21 | pub(crate) fn any_header_field_multi_line(i: &[u8]) -> IResult<&[u8], (&[u8], BString), Error> { | ||
| 22 | let (i, (k, o)) = peek(tuple(( | ||
| 23 | terminated(is_not(SPACE_OR_NL), tag(SPACE)), | ||
| 24 | recognize(tuple(( | ||
| 25 | is_not(NL), | ||
| 26 | tag(NL), | ||
| 27 | many1_count(terminated(tuple((tag(SPACE), take_until(NL))), tag(NL))), | ||
| 28 | ))), | ||
| 29 | )))(i)?; | ||
| 30 | assert!(!o.is_empty()); | ||
| 31 | let end = &o[o.len() - 1] as *const u8 as usize; | ||
| 32 | let start_input = &i[0] as *const u8 as usize; | ||
| 33 | |||
| 34 | let bytes = o[..o.len() - 1].as_bstr(); | ||
| 35 | let mut out = BString::from(Vec::with_capacity(bytes.len())); | ||
| 36 | let mut lines = bytes.lines(); | ||
| 37 | out.push_str(lines.next().expect("first line")); | ||
| 38 | for line in lines { | ||
| 39 | out.push(b'\n'); | ||
| 40 | out.push_str(&line[1..]); // cut leading space | ||
| 41 | } | ||
| 42 | Ok((&i[end - start_input + 1..], (k, out))) | ||
| 43 | } | ||
| 44 | |||
| 45 | pub(crate) fn header_field<'a, T>( | ||
| 46 | i: &'a [u8], | ||
| 47 | name: &'static [u8], | ||
| 48 | parse_value: impl Fn(&'a [u8]) -> IResult<&'a [u8], T, Error>, | ||
| 49 | ) -> IResult<&'a [u8], T, Error> { | ||
| 50 | terminated(preceded(terminated(tag(name), tag(SPACE)), parse_value), tag(NL))(i) | ||
| 51 | } | ||
| 52 | |||
| 53 | pub(crate) fn any_header_field<'a, T>( | ||
| 54 | i: &'a [u8], | ||
| 55 | parse_value: impl Fn(&'a [u8]) -> IResult<&'a [u8], T, Error>, | ||
| 56 | ) -> IResult<&'a [u8], (&'a [u8], T), Error> { | ||
| 57 | terminated( | ||
| 58 | tuple((terminated(is_not(SPACE_OR_NL), tag(SPACE)), parse_value)), | ||
| 59 | tag(NL), | ||
| 60 | )(i) | ||
| 61 | } | ||
| 62 | |||
| 63 | fn is_hex_digit_lc(b: u8) -> bool { | ||
| 64 | matches!(b, b'0'..=b'9' | b'a'..=b'f') | ||
| 65 | } | ||
| 66 | |||
| 67 | pub(crate) fn hex_sha1(i: &[u8]) -> IResult<&[u8], &BStr, Error> { | ||
| 68 | take_while_m_n(40usize, 40, is_hex_digit_lc)(i).map(|(i, o)| (i, o.as_bstr())) | ||
| 69 | } | ||
| 70 | |||
| 71 | pub(crate) fn signature(i: &[u8]) -> IResult<&[u8], Signature<'_>, Error> { | ||
| 72 | let (i, (name, email, time_in_seconds, tzsign, tzhour, tzminute)) = tuple(( | ||
| 73 | terminated(take_until(&b" <"[..]), take(2usize)), | ||
| 74 | terminated(take_until(&b"> "[..]), take(2usize)), | ||
| 75 | terminated(take_until(SPACE), take(1usize)), | ||
| 76 | alt((tag(b"-"), tag(b"+"))), | ||
| 77 | take_while_m_n(2usize, 2, is_digit), | ||
| 78 | take_while_m_n(2usize, 2, is_digit), | ||
| 79 | ))(i) | ||
| 80 | .map_err(Error::context( | ||
| 81 | "tagger <name> <<email>> <time seconds since epoch> <+|-><HHMM>", | ||
| 82 | ))?; | ||
| 83 | |||
| 84 | let sign = if tzsign[0] == b'-' { Sign::Minus } else { Sign::Plus }; | ||
| 85 | let hours = btoi::<i32>(&tzhour) | ||
| 86 | .map_err(|e| nom::Err::Error(Error::ParseIntegerError("invalid 'hours' string", tzhour.into(), e)))?; | ||
| 87 | let minutes = btoi::<i32>(&tzminute) | ||
| 88 | .map_err(|e| nom::Err::Error(Error::ParseIntegerError("invalid 'minutes' string", tzminute.into(), e)))?; | ||
| 89 | let offset = (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 }; | ||
| 90 | |||
| 91 | Ok(( | ||
| 92 | i, | ||
| 93 | Signature { | ||
| 94 | name: name.as_bstr(), | ||
| 95 | email: email.as_bstr(), | ||
| 96 | time: Time { | ||
| 97 | time: btoi::<u32>(time_in_seconds).map_err(|e| { | ||
| 98 | nom::Err::Error(Error::ParseIntegerError( | ||
| 99 | "Could parse to seconds", | ||
| 100 | time_in_seconds.into(), | ||
| 101 | e, | ||
| 102 | )) | ||
| 103 | })?, | ||
| 104 | offset, | ||
| 105 | sign, | ||
| 106 | }, | ||
| 107 | }, | ||
| 108 | )) | ||
| 109 | } | ||
| 110 | |||
| 111 | #[cfg(test)] | ||
| 112 | mod tests { | ||
| 113 | mod parse_signature { | ||
| 114 | use crate::{ | ||
| 115 | immutable::{parse, Signature}, | ||
| 116 | ByteSlice, Sign, Time, | ||
| 117 | }; | ||
| 118 | |||
| 119 | fn signature( | ||
| 120 | name: &'static str, | ||
| 121 | email: &'static str, | ||
| 122 | time: u32, | ||
| 123 | sign: Sign, | ||
| 124 | offset: i32, | ||
| 125 | ) -> Signature<'static> { | ||
| 126 | Signature { | ||
| 127 | name: name.as_bytes().as_bstr(), | ||
| 128 | email: email.as_bytes().as_bstr(), | ||
| 129 | time: Time { time, offset, sign }, | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | #[test] | ||
| 134 | fn tz_minus() { | ||
| 135 | assert_eq!( | ||
| 136 | parse::signature(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0230") | ||
| 137 | .expect("parse to work") | ||
| 138 | .1, | ||
| 139 | signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, -9000) | ||
| 140 | ); | ||
| 141 | } | ||
| 142 | |||
| 143 | #[test] | ||
| 144 | fn tz_plus() { | ||
| 145 | assert_eq!( | ||
| 146 | parse::signature(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 +0230") | ||
| 147 | .expect("parse to work") | ||
| 148 | .1, | ||
| 149 | signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Plus, 9000) | ||
| 150 | ); | ||
| 151 | } | ||
| 152 | |||
| 153 | #[test] | ||
| 154 | fn negative_offset_0000() { | ||
| 155 | assert_eq!( | ||
| 156 | parse::signature(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0000") | ||
| 157 | .expect("parse to work") | ||
| 158 | .1, | ||
| 159 | signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, 0) | ||
| 160 | ); | ||
| 161 | } | ||
| 162 | |||
| 163 | #[test] | ||
| 164 | fn empty_name_and_email() { | ||
| 165 | assert_eq!( | ||
| 166 | parse::signature(b" <> 12345 -1215").expect("parse to work").1, | ||
| 167 | signature("", "", 12345, Sign::Minus, -44100) | ||
| 168 | ); | ||
| 169 | } | ||
| 170 | } | ||
| 171 | } | ||
| @@ -0,0 +1,115 @@ | |||
| 1 | use crate::{ | ||
| 2 | immutable::{parse, parse::NL, Error, Signature}, | ||
| 3 | BStr, ByteSlice, | ||
| 4 | }; | ||
| 5 | use nom::bytes::complete::take_while; | ||
| 6 | use nom::{ | ||
| 7 | branch::alt, | ||
| 8 | bytes::complete::{tag, take_until, take_while1}, | ||
| 9 | character::is_alphabetic, | ||
| 10 | combinator::{all_consuming, opt, recognize}, | ||
| 11 | sequence::{preceded, tuple}, | ||
| 12 | IResult, | ||
| 13 | }; | ||
| 14 | |||
| 15 | /// Represents a git tag, commonly indicating a software release. | ||
| 16 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 17 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 18 | pub struct Tag<'a> { | ||
| 19 | /// The hash in hexadecimal being the object this tag points to. Use [`target()`][Tag::target()] to obtain a byte representation. | ||
| 20 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 21 | pub target: &'a BStr, | ||
| 22 | /// The name of the tag, e.g. "v1.0". | ||
| 23 | pub name: &'a BStr, | ||
| 24 | /// The kind of object that `target` points to. | ||
| 25 | pub target_kind: crate::Kind, | ||
| 26 | /// The message describing this release. | ||
| 27 | pub message: &'a BStr, | ||
| 28 | /// The author of the tag. | ||
| 29 | pub signature: Option<Signature<'a>>, | ||
| 30 | /// A cryptographic signature over the entire content of the serialized tag object thus far. | ||
| 31 | pub pgp_signature: Option<&'a BStr>, | ||
| 32 | } | ||
| 33 | |||
| 34 | impl<'a> Tag<'a> { | ||
| 35 | /// Deserialize a tag from `data`. | ||
| 36 | pub fn from_bytes(data: &'a [u8]) -> Result<Tag<'a>, Error> { | ||
| 37 | parse(data).map(|(_, t)| t).map_err(Error::from) | ||
| 38 | } | ||
| 39 | /// The object this tag points to as `Id`. | ||
| 40 | pub fn target(&self) -> git_hash::ObjectId { | ||
| 41 | git_hash::ObjectId::from_hex(self.target).expect("prior validation") | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | fn parse(i: &[u8]) -> IResult<&[u8], Tag<'_>, Error> { | ||
| 46 | let (i, target) = | ||
| 47 | parse::header_field(i, b"object", parse::hex_sha1).map_err(Error::context("object <40 lowercase hex char>"))?; | ||
| 48 | |||
| 49 | let (i, kind) = | ||
| 50 | parse::header_field(i, b"type", take_while1(is_alphabetic)).map_err(Error::context("type <object kind>"))?; | ||
| 51 | let kind = crate::Kind::from_bytes(kind).map_err(|e| nom::Err::Error(Error::ParseKindError(e)))?; | ||
| 52 | |||
| 53 | let (i, tag_version) = | ||
| 54 | parse::header_field(i, b"tag", take_while1(|b| b != NL[0])).map_err(Error::context("tag <version>"))?; | ||
| 55 | |||
| 56 | let (i, signature) = opt(|i| parse::header_field(i, b"tagger", parse::signature))(i) | ||
| 57 | .map_err(Error::context("tagger <signature>"))?; | ||
| 58 | let (i, (message, pgp_signature)) = all_consuming(parse_message)(i)?; | ||
| 59 | Ok(( | ||
| 60 | i, | ||
| 61 | Tag { | ||
| 62 | target, | ||
| 63 | name: tag_version.as_bstr(), | ||
| 64 | target_kind: kind, | ||
| 65 | message, | ||
| 66 | signature, | ||
| 67 | pgp_signature, | ||
| 68 | }, | ||
| 69 | )) | ||
| 70 | } | ||
| 71 | |||
| 72 | fn parse_message(i: &[u8]) -> IResult<&[u8], (&BStr, Option<&BStr>), Error> { | ||
| 73 | const PGP_SIGNATURE_BEGIN: &[u8] = b"\n-----BEGIN PGP SIGNATURE-----"; | ||
| 74 | const PGP_SIGNATURE_END: &[u8] = b"-----END PGP SIGNATURE-----"; | ||
| 75 | |||
| 76 | if i.is_empty() { | ||
| 77 | return Ok((i, (i.as_bstr(), None))); | ||
| 78 | } | ||
| 79 | let (i, _) = tag(NL)(i)?; | ||
| 80 | fn all_to_end(i: &[u8]) -> IResult<&[u8], (&[u8], &[u8]), Error> { | ||
| 81 | if i.is_empty() { | ||
| 82 | return Err(nom::Err::Error(Error::NomDetail(i.into(), "tag message is missing"))); | ||
| 83 | } | ||
| 84 | // an empty signature message signals that there is none - the function signature is needed | ||
| 85 | // to work with 'alt(…)'. PGP signatures are never empty | ||
| 86 | Ok((&[], (&i, &[]))) | ||
| 87 | } | ||
| 88 | let (i, (message, signature)) = alt(( | ||
| 89 | tuple(( | ||
| 90 | take_until(PGP_SIGNATURE_BEGIN), | ||
| 91 | preceded( | ||
| 92 | tag(NL), | ||
| 93 | recognize(tuple(( | ||
| 94 | tag(&PGP_SIGNATURE_BEGIN[1..]), | ||
| 95 | take_until(PGP_SIGNATURE_END), | ||
| 96 | tag(PGP_SIGNATURE_END), | ||
| 97 | take_while(|_| true), | ||
| 98 | ))), | ||
| 99 | ), | ||
| 100 | )), | ||
| 101 | all_to_end, | ||
| 102 | ))(i)?; | ||
| 103 | let (i, _) = opt(tag(NL))(i)?; | ||
| 104 | Ok(( | ||
| 105 | i, | ||
| 106 | ( | ||
| 107 | message.as_bstr(), | ||
| 108 | if signature.is_empty() { | ||
| 109 | None | ||
| 110 | } else { | ||
| 111 | Some(signature.as_bstr()) | ||
| 112 | }, | ||
| 113 | ), | ||
| 114 | )) | ||
| 115 | } | ||
| @@ -0,0 +1,79 @@ | |||
| 1 | use crate::{immutable::parse::SPACE, immutable::Error, tree}; | ||
| 2 | use bstr::{BStr, ByteSlice}; | ||
| 3 | use nom::{ | ||
| 4 | bytes::complete::{tag, take, take_while1, take_while_m_n}, | ||
| 5 | character::is_digit, | ||
| 6 | combinator::all_consuming, | ||
| 7 | multi::many1, | ||
| 8 | sequence::terminated, | ||
| 9 | IResult, | ||
| 10 | }; | ||
| 11 | use std::convert::TryFrom; | ||
| 12 | |||
| 13 | /// A directory snapshot containing files (blobs), directories (trees) and submodules (commits). | ||
| 14 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 15 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 16 | pub struct Tree<'a> { | ||
| 17 | /// The directories and files contained in this tree. | ||
| 18 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 19 | pub entries: Vec<Entry<'a>>, | ||
| 20 | } | ||
| 21 | |||
| 22 | /// An element of a [`Tree`][Tree::entries]. | ||
| 23 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 24 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 25 | pub struct Entry<'a> { | ||
| 26 | /// The kind of object to which `oid` is pointing. | ||
| 27 | pub mode: tree::Mode, | ||
| 28 | /// The name of the file in the parent tree. | ||
| 29 | pub filename: &'a BStr, | ||
| 30 | /// The id of the object representing the entry. | ||
| 31 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 32 | pub oid: &'a git_hash::oid, | ||
| 33 | } | ||
| 34 | |||
| 35 | impl<'a> Tree<'a> { | ||
| 36 | /// Deserialize a Tree from `data`. | ||
| 37 | pub fn from_bytes(data: &'a [u8]) -> Result<Tree<'a>, Error> { | ||
| 38 | parse(data).map(|(_, t)| t).map_err(Error::from) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | impl TryFrom<&[u8]> for tree::Mode { | ||
| 43 | type Error = Error; | ||
| 44 | |||
| 45 | fn try_from(mode: &[u8]) -> Result<Self, Self::Error> { | ||
| 46 | Ok(match mode { | ||
| 47 | b"40000" => tree::Mode::Tree, | ||
| 48 | b"100644" => tree::Mode::Blob, | ||
| 49 | b"100664" => tree::Mode::Blob, // rare and found in the linux kernel | ||
| 50 | b"100640" => tree::Mode::Blob, // rare and found in the Rust repo | ||
| 51 | b"100755" => tree::Mode::BlobExecutable, | ||
| 52 | b"120000" => tree::Mode::Link, | ||
| 53 | b"160000" => tree::Mode::Commit, | ||
| 54 | _ => return Err(Error::NomDetail(mode.into(), "unknown tree mode")), | ||
| 55 | }) | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | const NULL: &[u8] = b"\0"; | ||
| 60 | fn parse_entry(i: &[u8]) -> IResult<&[u8], Entry<'_>, Error> { | ||
| 61 | let (i, mode) = terminated(take_while_m_n(5, 6, is_digit), tag(SPACE))(i)?; | ||
| 62 | let mode = tree::Mode::try_from(mode).map_err(nom::Err::Error)?; | ||
| 63 | let (i, filename) = terminated(take_while1(|b| b != NULL[0]), tag(NULL))(i)?; | ||
| 64 | let (i, oid) = take(20u8)(i)?; | ||
| 65 | |||
| 66 | Ok(( | ||
| 67 | i, | ||
| 68 | Entry { | ||
| 69 | mode, | ||
| 70 | filename: filename.as_bstr(), | ||
| 71 | oid: git_hash::oid::try_from(oid).expect("we counted exactly 20 bytes"), | ||
| 72 | }, | ||
| 73 | )) | ||
| 74 | } | ||
| 75 | |||
| 76 | fn parse(i: &[u8]) -> IResult<&[u8], Tree<'_>, Error> { | ||
| 77 | let (i, entries) = all_consuming(many1(parse_entry))(i)?; | ||
| 78 | Ok((i, Tree { entries })) | ||
| 79 | } | ||