Skip to main content

tig_core/
object.rs

1//! git object (commit / tree / blob / tag) の解析。
2//!
3//! object の内容は「`<type> <size>\0` header + body」を SHA-1 したものが oid になる。
4//! 本モジュールの parse 関数は header を除いた body を対象とする。
5
6use 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
41/// body から oid を計算する。
42pub 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
57/// 10 進表記を buf へ書き、書いた桁数を返す。
58fn 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/// author / committer / tagger の署名行。
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Sig<'a> {
78    pub name: &'a [u8],
79    pub email: &'a [u8],
80    /// UNIX time (秒)。
81    pub time: i64,
82    /// タイムゾーン表記 (例: b"+0900")。
83    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    /// header と空行を除いたコミットメッセージ。
93    pub message: &'a [u8],
94}
95
96/// commit body を解析する。gpgsig 等の未知 header (継続行を含む) は読み飛ばす。
97pub 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        // 未知 header は無視する。継続行 (先頭が空白) も同じ経路で読み飛ばされる。
120    }
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
140/// annotated tag の body を解析する。
141pub 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
174/// tree body の entry を先頭から列挙する iterator。
175///
176/// entry は「`<mode> <name>\0` + oid (20 byte 生値)」の繰り返しで、名前順に並ぶ。
177pub struct TreeIter<'a> {
178    rest: &'a [u8],
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct TreeEntry<'a> {
183    /// 8 進表記のままの mode (例: b"100644"、b"40000")。
184    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
225// --- 内部ヘルパー -----------------------------------------------------------
226
227fn 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
235/// 次の LF までを返す。header 部の途中で入力が尽きた場合はエラー。
236fn 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
241/// `name <email> time tz` を解析する。
242fn 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    // "> " の後は "time tz"。tz が欠ける不正歴史も存在するため tz は任意とする。
249    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        // `git hash-object -t blob /dev/null` の既知値。
288        assert_eq!(
289            format!("{}", compute_oid(Kind::Blob, b"")),
290            "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
291        );
292    }
293
294    #[test]
295    fn oid_of_hello_blob() {
296        // `printf 'hello\n' | git hash-object --stdin` の既知値。
297        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}