1use std::collections::HashSet;
8
9use crate::change_id::ChangeId;
10use crate::fsck_standalone::{tree_entry_mode_allowed, MAX_TREE_ENTRY_LEN};
11use crate::ident::Signature;
12use crate::objects::{tree_entry_cmp, CommitData, HashAlgo, ObjectId, TreeEntry};
13use crate::path_validate::{audit_tree_entry_name, PathComponentError, ProtectOptions};
14
15const MODE_TREE: u32 = 0o040000;
16
17#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22#[non_exhaustive]
23pub enum TreeEntryError {
24 #[error("invalid tree entry name '{}': {reason}", String::from_utf8_lossy(.name))]
26 InvalidName {
27 name: Vec<u8>,
29 reason: PathComponentError,
31 },
32 #[error(
34 "tree entry name of {} bytes exceeds the {max}-byte limit",
35 .name.len()
36 )]
37 NameTooLong {
38 name: Vec<u8>,
40 max: usize,
42 },
43 #[error("tree entry '{}' has unsupported mode {mode:o}", String::from_utf8_lossy(.name))]
45 InvalidMode {
46 name: Vec<u8>,
48 mode: u32,
50 },
51 #[error("tree entry '{}' points at the null object id", String::from_utf8_lossy(.name))]
53 NullObjectId {
54 name: Vec<u8>,
56 },
57 #[error(
59 "tree entry '{}' has a {} object id in a {} repository",
60 String::from_utf8_lossy(.name),
61 .actual.name(),
62 .expected.name()
63 )]
64 HashAlgoMismatch {
65 name: Vec<u8>,
67 expected: HashAlgo,
69 actual: HashAlgo,
71 },
72 #[error("tree lists '{}' more than once", String::from_utf8_lossy(.name))]
74 DuplicateName {
75 name: Vec<u8>,
77 },
78}
79
80pub(crate) fn canonical_tree_entries(
90 entries: impl IntoIterator<Item = TreeEntry>,
91 algo: HashAlgo,
92) -> Result<Vec<TreeEntry>, TreeEntryError> {
93 let mut entries: Vec<TreeEntry> = entries.into_iter().collect();
94 for entry in &entries {
95 validate_tree_entry(entry, algo)?;
96 }
97 entries
98 .sort_by(|a, b| tree_entry_cmp(&a.name, a.mode == MODE_TREE, &b.name, b.mode == MODE_TREE));
99 let mut seen = HashSet::with_capacity(entries.len());
100 if let Some(dup) = entries.iter().find(|e| !seen.insert(e.name.as_slice())) {
101 return Err(TreeEntryError::DuplicateName {
102 name: dup.name.clone(),
103 });
104 }
105 Ok(entries)
106}
107
108fn validate_tree_entry(entry: &TreeEntry, algo: HashAlgo) -> Result<(), TreeEntryError> {
109 audit_tree_entry_name(&entry.name, ProtectOptions::fsck())
110 .first_error()
111 .map_err(|reason| TreeEntryError::InvalidName {
112 name: entry.name.clone(),
113 reason,
114 })?;
115 if entry.name.len() > MAX_TREE_ENTRY_LEN {
116 return Err(TreeEntryError::NameTooLong {
117 name: entry.name.clone(),
118 max: MAX_TREE_ENTRY_LEN,
119 });
120 }
121 if !tree_entry_mode_allowed(entry.mode) {
122 return Err(TreeEntryError::InvalidMode {
123 name: entry.name.clone(),
124 mode: entry.mode,
125 });
126 }
127 if entry.oid.algo() != algo {
128 return Err(TreeEntryError::HashAlgoMismatch {
129 name: entry.name.clone(),
130 expected: algo,
131 actual: entry.oid.algo(),
132 });
133 }
134 if entry.oid.is_zero() {
135 return Err(TreeEntryError::NullObjectId {
136 name: entry.name.clone(),
137 });
138 }
139 Ok(())
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct NewCommit {
177 tree: ObjectId,
178 parents: Vec<ObjectId>,
179 author: Signature,
180 committer: Signature,
181 message: String,
182 change_id: Option<ChangeId>,
183}
184
185impl NewCommit {
186 #[must_use]
194 pub fn new(
195 tree: ObjectId,
196 author: Signature,
197 committer: Signature,
198 message: impl Into<String>,
199 ) -> Self {
200 Self {
201 tree,
202 parents: Vec::new(),
203 author,
204 committer,
205 message: message.into(),
206 change_id: None,
207 }
208 }
209
210 #[must_use]
212 pub fn with_parents(mut self, parents: impl IntoIterator<Item = ObjectId>) -> Self {
213 self.parents = parents.into_iter().collect();
214 self
215 }
216
217 #[must_use]
223 pub fn with_change_id(mut self, id: ChangeId) -> Self {
224 self.change_id = Some(id);
225 self
226 }
227
228 pub(crate) fn change_id(&self) -> Option<&ChangeId> {
230 self.change_id.as_ref()
231 }
232
233 pub(crate) fn tree_and_parents(&self) -> (&ObjectId, &[ObjectId]) {
235 (&self.tree, &self.parents)
236 }
237
238 pub(crate) fn signatures(&self) -> [&Signature; 2] {
240 [&self.author, &self.committer]
241 }
242
243 pub(crate) fn to_commit_data(&self) -> CommitData {
245 let mut message = self.message.clone();
246 if !message.is_empty() && !message.ends_with('\n') {
247 message.push('\n');
248 }
249 CommitData {
250 tree: self.tree,
251 parents: self.parents.clone(),
252 author: self.author.header_line(),
253 committer: self.committer.header_line(),
254 author_raw: Vec::new(),
255 committer_raw: Vec::new(),
256 encoding: None,
257 message,
258 raw_message: None,
259 extra_headers: Vec::new(),
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use crate::objects::HashAlgo;
268 use time::OffsetDateTime;
269
270 fn oid(byte: u8) -> ObjectId {
271 ObjectId::from_bytes(&[byte; 20]).unwrap()
272 }
273
274 fn entry(mode: u32, name: &[u8], byte: u8) -> TreeEntry {
275 TreeEntry {
276 mode,
277 name: name.to_vec(),
278 oid: oid(byte),
279 }
280 }
281
282 #[test]
283 fn sorts_into_git_tree_order() {
284 let sorted = canonical_tree_entries(
285 [
286 entry(0o100644, b"foo.c", 1),
287 entry(MODE_TREE, b"foo", 2),
288 entry(0o100644, b"foo-bar", 3),
289 entry(0o100644, b"a", 4),
290 ],
291 HashAlgo::Sha1,
292 )
293 .unwrap();
294 let names: Vec<&[u8]> = sorted.iter().map(|e| e.name.as_slice()).collect();
295 assert_eq!(
296 names,
297 [&b"a"[..], &b"foo-bar"[..], &b"foo.c"[..], &b"foo"[..]]
298 );
299 }
300
301 #[test]
302 fn rejects_file_and_directory_with_the_same_name() {
303 let err = canonical_tree_entries(
304 [
305 entry(0o100644, b"foo", 1),
306 entry(0o100644, b"foo.c", 2),
307 entry(MODE_TREE, b"foo", 3),
308 ],
309 HashAlgo::Sha1,
310 )
311 .unwrap_err();
312 assert_eq!(
313 err,
314 TreeEntryError::DuplicateName {
315 name: b"foo".to_vec()
316 }
317 );
318 }
319
320 #[test]
321 fn rejects_bad_names_modes_and_ids() {
322 let cases: [(TreeEntry, TreeEntryError); 7] = [
323 (
324 entry(0o100644, &[b'x'; MAX_TREE_ENTRY_LEN + 1], 1),
325 TreeEntryError::NameTooLong {
326 name: vec![b'x'; MAX_TREE_ENTRY_LEN + 1],
327 max: MAX_TREE_ENTRY_LEN,
328 },
329 ),
330 (
331 entry(0o100644, b"", 1),
332 TreeEntryError::InvalidName {
333 name: Vec::new(),
334 reason: PathComponentError::Empty,
335 },
336 ),
337 (
338 entry(0o100644, b"a/b", 1),
339 TreeEntryError::InvalidName {
340 name: b"a/b".to_vec(),
341 reason: PathComponentError::ContainsSlash,
342 },
343 ),
344 (
345 entry(0o100644, b"..", 1),
346 TreeEntryError::InvalidName {
347 name: b"..".to_vec(),
348 reason: PathComponentError::DotDot,
349 },
350 ),
351 (
352 entry(MODE_TREE, b".GIT", 1),
353 TreeEntryError::InvalidName {
354 name: b".GIT".to_vec(),
355 reason: PathComponentError::DotGit,
356 },
357 ),
358 (
359 entry(0o100664, b"a", 1),
360 TreeEntryError::InvalidMode {
361 name: b"a".to_vec(),
362 mode: 0o100664,
363 },
364 ),
365 (
366 entry(0o100644, b"a", 0),
367 TreeEntryError::NullObjectId {
368 name: b"a".to_vec(),
369 },
370 ),
371 ];
372 for (input, expected) in cases {
373 assert_eq!(
374 canonical_tree_entries([input], HashAlgo::Sha1).unwrap_err(),
375 expected
376 );
377 }
378 }
379
380 #[test]
381 fn rejects_object_ids_of_the_other_hash_algorithm() {
382 let err = canonical_tree_entries([entry(0o100644, b"a", 1)], HashAlgo::Sha256).unwrap_err();
383 assert!(matches!(
384 err,
385 TreeEntryError::HashAlgoMismatch {
386 expected: HashAlgo::Sha256,
387 actual: HashAlgo::Sha1,
388 ..
389 }
390 ));
391 }
392
393 #[test]
394 fn commit_data_terminates_message_and_renders_signatures() {
395 let when = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
396 let sig = Signature::new("A U Thor", "a@example.com", when).unwrap();
397 let data = NewCommit::new(oid(1), sig.clone(), sig, "subject")
398 .with_parents([oid(2)])
399 .to_commit_data();
400 assert_eq!(data.message, "subject\n");
401 assert_eq!(data.parents, vec![oid(2)]);
402 assert_eq!(data.author, "A U Thor <a@example.com> 1700000000 +0000");
403 assert!(data.extra_headers.is_empty());
404 }
405}