1use alloc::vec::Vec;
7
8use crate::err::{Error, Result};
9use crate::oid::Oid;
10use crate::sha1;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Kind {
14 Commit,
15 Tree,
16 Blob,
17 Tag,
18}
19
20impl Kind {
21 pub const fn as_str(&self) -> &'static str {
22 match self {
23 Kind::Commit => "commit",
24 Kind::Tree => "tree",
25 Kind::Blob => "blob",
26 Kind::Tag => "tag",
27 }
28 }
29
30 pub fn from_name(name: &[u8]) -> Result<Self> {
31 match name {
32 b"commit" => Ok(Kind::Commit),
33 b"tree" => Ok(Kind::Tree),
34 b"blob" => Ok(Kind::Blob),
35 b"tag" => Ok(Kind::Tag),
36 _ => Err(Error::Corrupt("object type name")),
37 }
38 }
39}
40
41pub fn compute_oid(kind: Kind, body: &[u8]) -> Oid {
43 let mut header = [0u8; 32];
44 let mut n = 0;
45 for &b in kind.as_str().as_bytes() {
46 header[n] = b;
47 n += 1;
48 }
49 header[n] = b' ';
50 n += 1;
51 n += write_decimal(&mut header[n..], body.len());
52 header[n] = 0;
53 n += 1;
54 sha1::digest_oid(&[&header[..n], body])
55}
56
57fn write_decimal(buf: &mut [u8], mut value: usize) -> usize {
59 let mut digits = [0u8; 20];
60 let mut n = 0;
61 loop {
62 digits[n] = b'0' + (value % 10) as u8;
63 value /= 10;
64 n += 1;
65 if value == 0 {
66 break;
67 }
68 }
69 for i in 0..n {
70 buf[i] = digits[n - 1 - i];
71 }
72 n
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Sig<'a> {
78 pub name: &'a [u8],
79 pub email: &'a [u8],
80 pub time: i64,
82 pub tz: &'a [u8],
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Commit<'a> {
88 pub tree: Oid,
89 pub parents: Vec<Oid>,
90 pub author: Sig<'a>,
91 pub committer: Sig<'a>,
92 pub message: &'a [u8],
94}
95
96pub fn parse_commit(body: &[u8]) -> Result<Commit<'_>> {
98 let mut tree = None;
99 let mut parents = Vec::new();
100 let mut author = None;
101 let mut committer = None;
102
103 let mut rest = body;
104 loop {
105 let (line, next) = split_line(rest)?;
106 rest = next;
107 if line.is_empty() {
108 break;
109 }
110 if let Some(v) = strip_prefix(line, b"tree ") {
111 tree = Some(Oid::from_hex(v)?);
112 } else if let Some(v) = strip_prefix(line, b"parent ") {
113 parents.push(Oid::from_hex(v)?);
114 } else if let Some(v) = strip_prefix(line, b"author ") {
115 author = Some(parse_sig(v)?);
116 } else if let Some(v) = strip_prefix(line, b"committer ") {
117 committer = Some(parse_sig(v)?);
118 }
119 }
121
122 Ok(Commit {
123 tree: tree.ok_or(Error::Corrupt("commit without tree"))?,
124 parents,
125 author: author.ok_or(Error::Corrupt("commit without author"))?,
126 committer: committer.ok_or(Error::Corrupt("commit without committer"))?,
127 message: rest,
128 })
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct Tag<'a> {
133 pub object: Oid,
134 pub kind: Kind,
135 pub name: &'a [u8],
136 pub tagger: Option<Sig<'a>>,
137 pub message: &'a [u8],
138}
139
140pub fn parse_tag(body: &[u8]) -> Result<Tag<'_>> {
142 let mut object = None;
143 let mut kind = None;
144 let mut name = None;
145 let mut tagger = None;
146
147 let mut rest = body;
148 loop {
149 let (line, next) = split_line(rest)?;
150 rest = next;
151 if line.is_empty() {
152 break;
153 }
154 if let Some(v) = strip_prefix(line, b"object ") {
155 object = Some(Oid::from_hex(v)?);
156 } else if let Some(v) = strip_prefix(line, b"type ") {
157 kind = Some(Kind::from_name(v)?);
158 } else if let Some(v) = strip_prefix(line, b"tag ") {
159 name = Some(v);
160 } else if let Some(v) = strip_prefix(line, b"tagger ") {
161 tagger = Some(parse_sig(v)?);
162 }
163 }
164
165 Ok(Tag {
166 object: object.ok_or(Error::Corrupt("tag without object"))?,
167 kind: kind.ok_or(Error::Corrupt("tag without type"))?,
168 name: name.ok_or(Error::Corrupt("tag without name"))?,
169 tagger,
170 message: rest,
171 })
172}
173
174pub struct TreeIter<'a> {
178 rest: &'a [u8],
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct TreeEntry<'a> {
183 pub mode: &'a [u8],
185 pub name: &'a [u8],
186 pub oid: Oid,
187}
188
189impl<'a> TreeIter<'a> {
190 pub fn new(body: &'a [u8]) -> Self {
191 Self { rest: body }
192 }
193}
194
195impl<'a> Iterator for TreeIter<'a> {
196 type Item = Result<TreeEntry<'a>>;
197
198 fn next(&mut self) -> Option<Self::Item> {
199 if self.rest.is_empty() {
200 return None;
201 }
202 Some(self.parse_next())
203 }
204}
205
206impl<'a> TreeIter<'a> {
207 fn parse_next(&mut self) -> Result<TreeEntry<'a>> {
208 let sp = find(self.rest, b' ').ok_or(Error::Corrupt("tree entry mode"))?;
209 let mode = &self.rest[..sp];
210 let after_mode = &self.rest[sp + 1..];
211 let nul = find(after_mode, 0).ok_or(Error::Corrupt("tree entry name"))?;
212 let name = &after_mode[..nul];
213 let oid_bytes = after_mode
214 .get(nul + 1..nul + 21)
215 .ok_or(Error::UnexpectedEof)?;
216 self.rest = &after_mode[nul + 21..];
217 Ok(TreeEntry {
218 mode,
219 name,
220 oid: Oid::from_bytes(oid_bytes.try_into().unwrap()),
221 })
222 }
223}
224
225fn find(data: &[u8], byte: u8) -> Option<usize> {
228 data.iter().position(|&b| b == byte)
229}
230
231fn strip_prefix<'a>(line: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
232 line.strip_prefix(prefix)
233}
234
235fn split_line(data: &[u8]) -> Result<(&[u8], &[u8])> {
237 let nl = find(data, b'\n').ok_or(Error::Corrupt("unterminated header line"))?;
238 Ok((&data[..nl], &data[nl + 1..]))
239}
240
241fn parse_sig(v: &[u8]) -> Result<Sig<'_>> {
243 let lt = find(v, b'<').ok_or(Error::Corrupt("signature email start"))?;
244 let gt = lt + find(&v[lt..], b'>').ok_or(Error::Corrupt("signature email end"))?;
245 let name = v[..lt].strip_suffix(b" ").unwrap_or(&v[..lt]);
246 let email = &v[lt + 1..gt];
247
248 let rest = v.get(gt + 2..).unwrap_or(b"");
250 let (time_str, tz) = match find(rest, b' ') {
251 Some(sp) => (&rest[..sp], &rest[sp + 1..]),
252 None => (rest, &b""[..]),
253 };
254 let time = parse_i64(time_str).ok_or(Error::Corrupt("signature timestamp"))?;
255 Ok(Sig {
256 name,
257 email,
258 time,
259 tz,
260 })
261}
262
263fn parse_i64(s: &[u8]) -> Option<i64> {
264 let (neg, digits) = match s.split_first() {
265 Some((b'-', rest)) => (true, rest),
266 _ => (false, s),
267 };
268 if digits.is_empty() {
269 return None;
270 }
271 let mut value: i64 = 0;
272 for &c in digits {
273 if !c.is_ascii_digit() {
274 return None;
275 }
276 value = value.checked_mul(10)?.checked_add(i64::from(c - b'0'))?;
277 }
278 Some(if neg { -value } else { value })
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn oid_of_empty_blob() {
287 assert_eq!(
289 format!("{}", compute_oid(Kind::Blob, b"")),
290 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
291 );
292 }
293
294 #[test]
295 fn oid_of_hello_blob() {
296 assert_eq!(
298 format!("{}", compute_oid(Kind::Blob, b"hello\n")),
299 "ce013625030ba8dba906f756967f9e9ca394464a"
300 );
301 }
302
303 #[test]
304 fn parse_minimal_commit() {
305 let body = b"tree a9993e364706816aba3e25717850c26c9cd0d89d\n\
306 parent e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n\
307 author Alice <alice@example.com> 1700000000 +0900\n\
308 committer Bob <bob@example.com> 1700000100 -0500\n\
309 \n\
310 subject\n\nbody\n";
311 let c = parse_commit(body).unwrap();
312 assert_eq!(c.parents.len(), 1);
313 assert_eq!(c.author.name, b"Alice");
314 assert_eq!(c.author.email, b"alice@example.com");
315 assert_eq!(c.author.time, 1_700_000_000);
316 assert_eq!(c.author.tz, b"+0900");
317 assert_eq!(c.committer.time, 1_700_000_100);
318 assert_eq!(c.message, b"subject\n\nbody\n");
319 }
320
321 #[test]
322 fn parse_commit_skips_gpgsig() {
323 let body = b"tree a9993e364706816aba3e25717850c26c9cd0d89d\n\
324 author A <a@e> 1 +0000\n\
325 committer A <a@e> 1 +0000\n\
326 gpgsig -----BEGIN PGP SIGNATURE-----\n\
327 lines\n\
328 -----END PGP SIGNATURE-----\n\
329 \n\
330 msg";
331 let c = parse_commit(body).unwrap();
332 assert_eq!(c.message, b"msg");
333 assert!(c.parents.is_empty());
334 }
335
336 #[test]
337 fn tree_iteration() {
338 let oid = Oid::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap();
339 let mut body = Vec::new();
340 body.extend_from_slice(b"100644 a.txt\0");
341 body.extend_from_slice(oid.as_bytes());
342 body.extend_from_slice(b"40000 dir\0");
343 body.extend_from_slice(oid.as_bytes());
344
345 let entries: Vec<_> = TreeIter::new(&body).collect::<Result<_>>().unwrap();
346 assert_eq!(entries.len(), 2);
347 assert_eq!(entries[0].mode, b"100644");
348 assert_eq!(entries[0].name, b"a.txt");
349 assert_eq!(entries[1].mode, b"40000");
350 assert_eq!(entries[1].oid, oid);
351 }
352}