History

rename git-object::(owned->mutable)|(borrowed|immutable) #(67)

This categorizes them semantically without using the overloaded names of owning and borrowing, which mean different things in Rust.

Sebastian Thiel committed 5 years ago

Commit 91ee5581 parent cd7356034 files · +149 −141Browse files

Showing the changes to git-object/src/immutable (8 of 34 files). Show all

Unified · Split

8 files
  1. git-object/src/immutable/blob.rs +16 −0
  2. git-object/src/immutable/commit.rs +109 −0
  3. git-object/src/immutable/mod.rs +22 −0
  4. git-object/src/immutable/object.rs +184 −0
  5. git-object/src/immutable/object/error.rs +59 −0
  6. git-object/src/immutable/parse.rs +171 −0
  7. git-object/src/immutable/tag.rs +115 −0
  8. git-object/src/immutable/tree.rs +79 −0
git-object/src/immutable/blob.rs+16−0
@@ -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+}
git-object/src/immutable/commit.rs+109−0
@@ -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+}
git-object/src/immutable/mod.rs+22−0
@@ -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;
git-object/src/immutable/object.rs+184−0
@@ -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+}
git-object/src/immutable/object/error.rs+59−0
@@ -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+}
git-object/src/immutable/parse.rs+171−0
@@ -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+}
git-object/src/immutable/tag.rs+115−0
@@ -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+}
git-object/src/immutable/tree.rs+79−0
@@ -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+}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t