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 | +} | |