1use alloc::vec::Vec;
8
9use crate::err::{Error, Result};
10use crate::object::Sig;
11use crate::oid::Oid;
12
13#[derive(Debug, Clone)]
15pub struct TreeEntry<'a> {
16 pub mode: &'a [u8],
17 pub name: &'a [u8],
18 pub oid: Oid,
19}
20
21pub 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
52fn 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
68pub 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
94fn push_sig(out: &mut Vec<u8>, sig: &Sig<'_>) -> Result<()> {
96 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 #[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 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 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}