1use crate::{HashKind, ObjectId, Result, error::invalid};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ObjectKind {
5 Commit,
6 Tree,
7 Blob,
8 Tag,
9}
10
11impl ObjectKind {
12 pub(crate) fn parse(value: &[u8]) -> Result<Self> {
13 match value {
14 b"commit" => Ok(Self::Commit),
15 b"tree" => Ok(Self::Tree),
16 b"blob" => Ok(Self::Blob),
17 b"tag" => Ok(Self::Tag),
18 _ => Err(invalid(format!(
19 "unknown object kind {}",
20 String::from_utf8_lossy(value)
21 ))),
22 }
23 }
24
25 #[must_use]
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::Commit => "commit",
29 Self::Tree => "tree",
30 Self::Blob => "blob",
31 Self::Tag => "tag",
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Object {
38 pub id: ObjectId,
39 pub kind: ObjectKind,
40 pub data: Vec<u8>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Signature {
45 pub name: String,
46 pub email: String,
47 pub timestamp: i64,
48 pub timezone_minutes: i16,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Commit {
53 pub id: ObjectId,
54 pub tree: ObjectId,
55 pub parents: Vec<ObjectId>,
56 pub author: Option<Signature>,
57 pub committer: Option<Signature>,
58 pub encoding: Option<String>,
59 pub message: Vec<u8>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct CommitMetadata {
64 pub id: ObjectId,
65 pub tree: ObjectId,
66 pub parents: Vec<ObjectId>,
67 pub committer_time: i64,
68}
69
70impl Commit {
71 pub(crate) fn parse(id: ObjectId, data: &[u8], max_parents: usize) -> Result<Self> {
72 let (headers, message) = split_headers(data)?;
73 let mut tree = None;
74 let mut parents = Vec::new();
75 let mut author = None;
76 let mut committer = None;
77 let mut encoding = None;
78 for (name, value) in headers {
79 match name {
80 b"tree" => tree = Some(parse_oid(value, id.kind())?),
81 b"parent" => {
82 if parents.len() >= max_parents {
83 return Err(crate::GitError::LimitExceeded {
84 resource: "commit parents",
85 limit: max_parents,
86 });
87 }
88 parents.push(parse_oid(value, id.kind())?);
89 }
90 b"author" => author = parse_signature(value),
91 b"committer" => committer = parse_signature(value),
92 b"encoding" => encoding = Some(String::from_utf8_lossy(value).into_owned()),
93 _ => {}
94 }
95 }
96 Ok(Self {
97 id,
98 tree: tree.ok_or_else(|| invalid("commit has no tree header"))?,
99 parents,
100 author,
101 committer,
102 encoding,
103 message: message.to_vec(),
104 })
105 }
106
107 #[must_use]
108 pub fn message_lossy(&self) -> String {
109 String::from_utf8_lossy(&self.message).into_owned()
110 }
111
112 #[must_use]
113 pub fn summary_lossy(&self) -> String {
114 let message = String::from_utf8_lossy(&self.message);
115 message.lines().next().unwrap_or_default().to_owned()
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct Tag {
121 pub id: ObjectId,
122 pub target: ObjectId,
123 pub target_kind: ObjectKind,
124 pub name: String,
125 pub tagger: Option<Signature>,
126 pub message: Vec<u8>,
127}
128
129impl Tag {
130 pub(crate) fn parse(id: ObjectId, data: &[u8]) -> Result<Self> {
131 let (headers, message) = split_headers(data)?;
132 let mut target = None;
133 let mut target_kind = None;
134 let mut name = None;
135 let mut tagger = None;
136 for (key, value) in headers {
137 match key {
138 b"object" => target = Some(parse_oid(value, id.kind())?),
139 b"type" => target_kind = Some(ObjectKind::parse(value)?),
140 b"tag" => name = Some(String::from_utf8_lossy(value).into_owned()),
141 b"tagger" => tagger = parse_signature(value),
142 _ => {}
143 }
144 }
145 Ok(Self {
146 id,
147 target: target.ok_or_else(|| invalid("tag has no object header"))?,
148 target_kind: target_kind.ok_or_else(|| invalid("tag has no type header"))?,
149 name: name.ok_or_else(|| invalid("tag has no tag header"))?,
150 tagger,
151 message: message.to_vec(),
152 })
153 }
154}
155
156type Headers<'a> = Vec<(&'a [u8], &'a [u8])>;
157
158fn split_headers(data: &[u8]) -> Result<(Headers<'_>, &[u8])> {
159 let separator = data
160 .windows(2)
161 .position(|value| value == b"\n\n")
162 .ok_or_else(|| invalid("object headers are not terminated"))?;
163 let mut headers = Vec::new();
164 for line in data[..separator].split(|byte| *byte == b'\n') {
165 if line.first() == Some(&b' ') {
166 continue;
167 }
168 let split = line
169 .iter()
170 .position(|byte| *byte == b' ')
171 .ok_or_else(|| invalid("object header has no value"))?;
172 headers.push((&line[..split], &line[split + 1..]));
173 }
174 Ok((headers, &data[separator + 2..]))
175}
176
177fn parse_oid(value: &[u8], kind: HashKind) -> Result<ObjectId> {
178 ObjectId::from_hex_for(
179 std::str::from_utf8(value).map_err(|_| invalid("object id is not ASCII"))?,
180 kind,
181 )
182}
183
184pub(crate) fn parse_signature(value: &[u8]) -> Option<Signature> {
185 let text = std::str::from_utf8(value).ok()?;
186 let email_start = text.rfind(" <")?;
187 let email_end = text[email_start + 2..]
188 .find("> ")?
189 .saturating_add(email_start + 2);
190 let mut tail = text[email_end + 2..].split_ascii_whitespace();
191 let timestamp = tail.next()?.parse().ok()?;
192 let timezone = tail.next()?;
193 if timezone.len() != 5 {
194 return None;
195 }
196 let sign = if timezone.starts_with('-') { -1 } else { 1 };
197 let hours: i16 = timezone[1..3].parse().ok()?;
198 let minutes: i16 = timezone[3..5].parse().ok()?;
199 Some(Signature {
200 name: text[..email_start].to_owned(),
201 email: text[email_start + 2..email_end].to_owned(),
202 timestamp,
203 timezone_minutes: sign * (hours * 60 + minutes),
204 })
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn parses_commit_headers_and_signature() {
213 let id: ObjectId = "1111111111111111111111111111111111111111".parse().unwrap();
214 let tree = "2222222222222222222222222222222222222222";
215 let raw = format!(
216 "tree {tree}\nauthor Ada <ada@example.com> 42 +0230\ncommitter Ada <ada@example.com> 43 +0230\n\nsubject\nbody\n"
217 );
218 let commit = Commit::parse(id, raw.as_bytes(), 16).unwrap();
219 assert_eq!(commit.tree.to_string(), tree);
220 assert_eq!(commit.summary_lossy(), "subject");
221 assert_eq!(commit.author.unwrap().timezone_minutes, 150);
222 }
223
224 #[test]
225 fn parses_parents_encoding_and_annotated_tag() {
226 let id: ObjectId = "1111111111111111111111111111111111111111".parse().unwrap();
227 let target = "2222222222222222222222222222222222222222";
228 let raw = format!(
229 "tree {target}\nparent {target}\nencoding ISO-8859-1\n\
230 author Invalid\ncommitter Ada <ada@example.com> 43 -0130\n\nmessage"
231 );
232 let commit = Commit::parse(id, raw.as_bytes(), 2).unwrap();
233 assert_eq!(commit.parents.len(), 1);
234 assert_eq!(commit.encoding.as_deref(), Some("ISO-8859-1"));
235 assert!(commit.author.is_none());
236 assert_eq!(commit.committer.as_ref().unwrap().timezone_minutes, -90);
237 assert_eq!(commit.message_lossy(), "message");
238
239 let raw = format!(
240 "object {target}\ntype blob\ntag v1.0\n\
241 tagger Ada <ada@example.com> 44 +0000\n\nrelease"
242 );
243 let tag = Tag::parse(id, raw.as_bytes()).unwrap();
244 assert_eq!(tag.target.to_string(), target);
245 assert_eq!(tag.target_kind, ObjectKind::Blob);
246 assert_eq!(tag.name, "v1.0");
247 assert_eq!(tag.message, b"release");
248 }
249
250 #[test]
251 fn rejects_malformed_objects_and_parent_limit() {
252 let id: ObjectId = "1111111111111111111111111111111111111111".parse().unwrap();
253 assert!(Commit::parse(id, b"author x\n\nmessage", 1).is_err());
254 let target = "2222222222222222222222222222222222222222";
255 let raw = format!("tree {target}\nparent {target}\nparent {target}\n\nmessage");
256 assert!(Commit::parse(id, raw.as_bytes(), 1).is_err());
257 assert!(Tag::parse(id, b"object bad\n\nmessage").is_err());
258 assert!(ObjectKind::parse(b"unknown").is_err());
259 assert_eq!(ObjectKind::Tree.as_str(), "tree");
260 assert_eq!(ObjectKind::Tag.as_str(), "tag");
261 }
262}