Skip to main content

tig_core/
build.rs

1//! object (tree / commit / tag) の生成。
2//!
3//! 生成する body は `object` module の parse 対象と同じ形式で、oid は
4//! `object::compute_oid` で求める。blob の body は内容そのものであり、
5//! 専用の builder を持たない。
6
7use alloc::vec::Vec;
8
9use crate::err::{Error, Result};
10use crate::object::Sig;
11use crate::oid::Oid;
12
13/// tree に登録する entry。mode は 8 進表記のまま渡す (例: b"100644"、b"40000")。
14#[derive(Debug, Clone)]
15pub struct TreeEntry<'a> {
16    pub mode: &'a [u8],
17    pub name: &'a [u8],
18    pub oid: Oid,
19}
20
21/// tree の body を生成する。entry は git の正規順 (directory は名前に '/' を
22/// 補って比較する) に並べ替える。
23pub fn tree(entries: &[TreeEntry<'_>]) -> Result<Vec<u8>> {
24    for entry in entries {
25        if entry.name.is_empty()
26            || entry.name == b"."
27            || entry.name == b".."
28            || entry.name.contains(&b'/')
29            || entry.name.contains(&0)
30        {
31            return Err(Error::Corrupt("tree entry name"));
32        }
33        if entry.mode.is_empty() || !entry.mode.iter().all(|b| (b'0'..=b'7').contains(b)) {
34            return Err(Error::Corrupt("tree entry mode"));
35        }
36    }
37
38    let mut sorted: Vec<&TreeEntry<'_>> = entries.iter().collect();
39    sorted.sort_by(|a, b| cmp_tree(a, b));
40
41    let mut out = Vec::new();
42    for entry in sorted {
43        out.extend_from_slice(entry.mode);
44        out.push(b' ');
45        out.extend_from_slice(entry.name);
46        out.push(0);
47        out.extend_from_slice(entry.oid.as_bytes());
48    }
49    Ok(out)
50}
51
52/// git の tree 順: directory は名前の末尾に '/' を補ったものとして byte 比較する
53/// (例: "foo.txt" < "foo" (dir)、'.' 0x2e < '/' 0x2f のため)。
54fn cmp_tree(a: &TreeEntry<'_>, b: &TreeEntry<'_>) -> core::cmp::Ordering {
55    let pad = |e: &TreeEntry<'_>| if is_dir(e) { b'/' } else { 0 };
56    let min = a.name.len().min(b.name.len());
57    a.name[..min].cmp(&b.name[..min]).then_with(|| {
58        let ac = a.name.get(min).copied().unwrap_or_else(|| pad(a));
59        let bc = b.name.get(min).copied().unwrap_or_else(|| pad(b));
60        ac.cmp(&bc)
61    })
62}
63
64fn is_dir(entry: &TreeEntry<'_>) -> bool {
65    entry.mode == b"40000" || entry.mode == b"040000"
66}
67
68/// commit の body を生成する。
69pub fn commit(
70    tree: Oid,
71    parents: &[Oid],
72    author: &Sig<'_>,
73    committer: &Sig<'_>,
74    message: &[u8],
75) -> Result<Vec<u8>> {
76    let mut out = Vec::new();
77    out.extend_from_slice(b"tree ");
78    push_hex(&mut out, &tree);
79    out.push(b'\n');
80    for parent in parents {
81        out.extend_from_slice(b"parent ");
82        push_hex(&mut out, parent);
83        out.push(b'\n');
84    }
85    out.extend_from_slice(b"author ");
86    push_sig(&mut out, author)?;
87    out.extend_from_slice(b"committer ");
88    push_sig(&mut out, committer)?;
89    out.push(b'\n');
90    out.extend_from_slice(message);
91    Ok(out)
92}
93
94/// `name <email> time tz` + LF。
95fn push_sig(out: &mut Vec<u8>, sig: &Sig<'_>) -> Result<()> {
96    // 形式を壊す byte を拒否する (git の fsck と同等の最低限)。
97    let bad = |field: &[u8]| field.iter().any(|&b| b == b'<' || b == b'>' || b == b'\n');
98    if bad(sig.name) || bad(sig.email) || sig.tz.contains(&b'\n') {
99        return Err(Error::Corrupt("signature field"));
100    }
101    if !sig.name.is_empty() {
102        out.extend_from_slice(sig.name);
103        out.push(b' ');
104    }
105    out.push(b'<');
106    out.extend_from_slice(sig.email);
107    out.extend_from_slice(b"> ");
108    push_i64(out, sig.time);
109    out.push(b' ');
110    out.extend_from_slice(sig.tz);
111    out.push(b'\n');
112    Ok(())
113}
114
115fn push_hex(out: &mut Vec<u8>, oid: &Oid) {
116    const HEX: &[u8; 16] = b"0123456789abcdef";
117    for &b in oid.as_bytes() {
118        out.push(HEX[usize::from(b >> 4)]);
119        out.push(HEX[usize::from(b & 0xf)]);
120    }
121}
122
123fn push_i64(out: &mut Vec<u8>, value: i64) {
124    if value < 0 {
125        out.push(b'-');
126    }
127    let mut v = value.unsigned_abs();
128    let mut digits = [0u8; 20];
129    let mut n = 0;
130    loop {
131        digits[n] = b'0' + (v % 10) as u8;
132        v /= 10;
133        n += 1;
134        if v == 0 {
135            break;
136        }
137    }
138    for i in (0..n).rev() {
139        out.push(digits[i]);
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::object::{self, Kind};
147
148    fn oid(n: u8) -> Oid {
149        Oid::from_bytes([n; 20])
150    }
151
152    // 生成した body が自前の parser で往復できること。
153    #[test]
154    fn commit_roundtrip() {
155        let author = Sig {
156            name: b"Alice",
157            email: b"alice@example.com",
158            time: 1_700_000_000,
159            tz: b"+0900",
160        };
161        let body = commit(oid(1), &[oid(2), oid(3)], &author, &author, b"subject\n").unwrap();
162        let parsed = object::parse_commit(&body).unwrap();
163        assert_eq!(parsed.tree, oid(1));
164        assert_eq!(parsed.parents, alloc::vec![oid(2), oid(3)]);
165        assert_eq!(parsed.author, author);
166        assert_eq!(parsed.message, b"subject\n");
167    }
168
169    #[test]
170    fn tree_sorts_directories_padded() {
171        // "foo" (dir) は "foo/" として比較され、"foo.txt" より後になる。
172        let entries = [
173            TreeEntry {
174                mode: b"40000",
175                name: b"foo",
176                oid: oid(1),
177            },
178            TreeEntry {
179                mode: b"100644",
180                name: b"foo.txt",
181                oid: oid(2),
182            },
183        ];
184        let body = tree(&entries).unwrap();
185        let names: Vec<&[u8]> = object::TreeIter::new(&body)
186            .map(|e| e.unwrap().name)
187            .collect();
188        assert_eq!(names, alloc::vec![&b"foo.txt"[..], &b"foo"[..]]);
189        // parse も往復できること。
190        assert_eq!(object::compute_oid(Kind::Tree, &body), {
191            let again = tree(&entries).unwrap();
192            object::compute_oid(Kind::Tree, &again)
193        });
194    }
195
196    #[test]
197    fn invalid_names_rejected() {
198        for name in [&b""[..], b".", b"..", b"a/b", b"a\0b"] {
199            let entries = [TreeEntry {
200                mode: b"100644",
201                name,
202                oid: oid(1),
203            }];
204            assert!(tree(&entries).is_err(), "name={name:?}");
205        }
206    }
207
208    #[test]
209    fn invalid_signature_rejected() {
210        let bad = Sig {
211            name: b"a<b",
212            email: b"e",
213            time: 0,
214            tz: b"+0000",
215        };
216        assert!(commit(oid(1), &[], &bad, &bad, b"m").is_err());
217    }
218}