1use std::collections::HashSet;
8
9use crate::change_id::ChangeId;
10use crate::error::Error;
11use crate::fsck_standalone::{tree_entry_mode_allowed, MAX_TREE_ENTRY_LEN};
12use crate::gitmodules::tree_entry_reserved_dot_symlink;
13use crate::ident::Signature;
14use crate::objects::{tree_entry_cmp, CommitData, HashAlgo, ObjectId, ObjectKind, TreeEntry};
15use crate::odb::Odb;
16use crate::path_validate::{audit_tree_entry_name, PathComponentError, ProtectOptions};
17
18const MODE_TREE: u32 = 0o040000;
19const MODE_GITLINK: u32 = 0o160000;
20
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
26#[non_exhaustive]
27pub enum TreeEntryError {
28 #[error("invalid tree entry name '{}': {reason}", String::from_utf8_lossy(.name))]
30 InvalidName {
31 name: Vec<u8>,
33 reason: PathComponentError,
35 },
36 #[error(
38 "tree entry name of {} bytes exceeds the {max}-byte limit",
39 .name.len()
40 )]
41 NameTooLong {
42 name: Vec<u8>,
44 max: usize,
46 },
47 #[error("tree entry '{}' has unsupported mode {mode:o}", String::from_utf8_lossy(.name))]
49 InvalidMode {
50 name: Vec<u8>,
52 mode: u32,
54 },
55 #[error("tree entry '{}' points at the null object id", String::from_utf8_lossy(.name))]
57 NullObjectId {
58 name: Vec<u8>,
60 },
61 #[error(
63 "tree entry '{}' has a {} object id in a {} repository",
64 String::from_utf8_lossy(.name),
65 .actual.name(),
66 .expected.name()
67 )]
68 HashAlgoMismatch {
69 name: Vec<u8>,
71 expected: HashAlgo,
73 actual: HashAlgo,
75 },
76 #[error("tree lists '{}' more than once", String::from_utf8_lossy(.name))]
78 DuplicateName {
79 name: Vec<u8>,
81 },
82 #[error(
84 "tree entry '{}' must not be a symlink to {reserved_file}",
85 String::from_utf8_lossy(.name)
86 )]
87 ForbiddenDotSymlink {
88 name: Vec<u8>,
90 reserved_file: &'static str,
92 },
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
97pub struct WriteTreeOptions {
98 pub allow_missing: bool,
101}
102
103pub(crate) fn canonical_tree_entries(
113 entries: impl IntoIterator<Item = TreeEntry>,
114 algo: HashAlgo,
115) -> Result<Vec<TreeEntry>, TreeEntryError> {
116 let mut entries: Vec<TreeEntry> = entries.into_iter().collect();
117 for entry in &entries {
118 validate_tree_entry(entry, algo)?;
119 }
120 entries
121 .sort_by(|a, b| tree_entry_cmp(&a.name, a.mode == MODE_TREE, &b.name, b.mode == MODE_TREE));
122 let mut seen = HashSet::with_capacity(entries.len());
123 if let Some(dup) = entries.iter().find(|e| !seen.insert(e.name.as_slice())) {
124 return Err(TreeEntryError::DuplicateName {
125 name: dup.name.clone(),
126 });
127 }
128 Ok(entries)
129}
130
131pub(crate) fn validate_tree_entry_stored_kinds(
138 odb: &Odb,
139 entries: &[TreeEntry],
140 allow_missing: bool,
141) -> Result<(), Error> {
142 for entry in entries {
143 validate_one_tree_entry_stored_kind(odb, entry, allow_missing)?;
144 }
145 Ok(())
146}
147
148fn expected_kind_for_tree_mode(mode: u32) -> ObjectKind {
149 match mode {
150 MODE_TREE => ObjectKind::Tree,
151 MODE_GITLINK => ObjectKind::Commit,
152 _ => ObjectKind::Blob,
153 }
154}
155
156fn validate_one_tree_entry_stored_kind(
157 odb: &Odb,
158 entry: &TreeEntry,
159 allow_missing: bool,
160) -> Result<(), Error> {
161 let expected = expected_kind_for_tree_mode(entry.mode);
162 let stored = odb.stored_object_kind(&entry.oid)?;
163 if entry.mode == MODE_GITLINK {
164 if let Some(actual) = stored {
165 if actual != ObjectKind::Commit {
166 return Err(Error::UnexpectedObjectKind {
167 oid: entry.oid,
168 expected: ObjectKind::Commit,
169 actual,
170 });
171 }
172 }
173 return Ok(());
174 }
175 let Some(actual) = stored else {
176 if allow_missing {
177 return Ok(());
178 }
179 return Err(Error::ObjectNotFound(entry.oid.to_hex()));
180 };
181 if actual != expected {
182 return Err(Error::UnexpectedObjectKind {
183 oid: entry.oid,
184 expected,
185 actual,
186 });
187 }
188 Ok(())
189}
190
191fn validate_tree_entry(entry: &TreeEntry, algo: HashAlgo) -> Result<(), TreeEntryError> {
192 audit_tree_entry_name(&entry.name, ProtectOptions::fsck())
193 .first_error()
194 .map_err(|reason| TreeEntryError::InvalidName {
195 name: entry.name.clone(),
196 reason,
197 })?;
198 if entry.name.len() > MAX_TREE_ENTRY_LEN {
199 return Err(TreeEntryError::NameTooLong {
200 name: entry.name.clone(),
201 max: MAX_TREE_ENTRY_LEN,
202 });
203 }
204 if !tree_entry_mode_allowed(entry.mode) {
205 return Err(TreeEntryError::InvalidMode {
206 name: entry.name.clone(),
207 mode: entry.mode,
208 });
209 }
210 if entry.oid.algo() != algo {
211 return Err(TreeEntryError::HashAlgoMismatch {
212 name: entry.name.clone(),
213 expected: algo,
214 actual: entry.oid.algo(),
215 });
216 }
217 if entry.oid.is_zero() {
218 return Err(TreeEntryError::NullObjectId {
219 name: entry.name.clone(),
220 });
221 }
222 if let Some(reserved_file) = tree_entry_reserved_dot_symlink(entry.mode, &entry.name) {
223 return Err(TreeEntryError::ForbiddenDotSymlink {
224 name: entry.name.clone(),
225 reserved_file,
226 });
227 }
228 Ok(())
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct NewCommit {
266 tree: ObjectId,
267 parents: Vec<ObjectId>,
268 author: Signature,
269 committer: Signature,
270 message: String,
271 change_id: Option<ChangeId>,
272}
273
274impl NewCommit {
275 #[must_use]
283 pub fn new(
284 tree: ObjectId,
285 author: Signature,
286 committer: Signature,
287 message: impl Into<String>,
288 ) -> Self {
289 Self {
290 tree,
291 parents: Vec::new(),
292 author,
293 committer,
294 message: message.into(),
295 change_id: None,
296 }
297 }
298
299 #[must_use]
301 pub fn with_parents(mut self, parents: impl IntoIterator<Item = ObjectId>) -> Self {
302 self.parents = parents.into_iter().collect();
303 self
304 }
305
306 #[must_use]
312 pub fn with_change_id(mut self, id: ChangeId) -> Self {
313 self.change_id = Some(id);
314 self
315 }
316
317 pub(crate) fn change_id(&self) -> Option<&ChangeId> {
319 self.change_id.as_ref()
320 }
321
322 pub(crate) fn tree_and_parents(&self) -> (&ObjectId, &[ObjectId]) {
324 (&self.tree, &self.parents)
325 }
326
327 pub(crate) fn signatures(&self) -> [&Signature; 2] {
329 [&self.author, &self.committer]
330 }
331
332 pub(crate) fn to_commit_data(&self) -> CommitData {
334 let mut message = self.message.clone();
335 if !message.is_empty() && !message.ends_with('\n') {
336 message.push('\n');
337 }
338 CommitData {
339 tree: self.tree,
340 parents: self.parents.clone(),
341 author: self.author.header_line(),
342 committer: self.committer.header_line(),
343 author_raw: Vec::new(),
344 committer_raw: Vec::new(),
345 encoding: None,
346 message,
347 raw_message: None,
348 extra_headers: Vec::new(),
349 }
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::objects::HashAlgo;
357 use time::OffsetDateTime;
358
359 fn oid(byte: u8) -> ObjectId {
360 ObjectId::from_bytes(&[byte; 20]).unwrap()
361 }
362
363 fn entry(mode: u32, name: &[u8], byte: u8) -> TreeEntry {
364 TreeEntry {
365 mode,
366 name: name.to_vec(),
367 oid: oid(byte),
368 }
369 }
370
371 #[test]
372 fn sorts_into_git_tree_order() {
373 let sorted = canonical_tree_entries(
374 [
375 entry(0o100644, b"foo.c", 1),
376 entry(MODE_TREE, b"foo", 2),
377 entry(0o100644, b"foo-bar", 3),
378 entry(0o100644, b"a", 4),
379 ],
380 HashAlgo::Sha1,
381 )
382 .unwrap();
383 let names: Vec<&[u8]> = sorted.iter().map(|e| e.name.as_slice()).collect();
384 assert_eq!(
385 names,
386 [&b"a"[..], &b"foo-bar"[..], &b"foo.c"[..], &b"foo"[..]]
387 );
388 }
389
390 #[test]
391 fn rejects_file_and_directory_with_the_same_name() {
392 let err = canonical_tree_entries(
393 [
394 entry(0o100644, b"foo", 1),
395 entry(0o100644, b"foo.c", 2),
396 entry(MODE_TREE, b"foo", 3),
397 ],
398 HashAlgo::Sha1,
399 )
400 .unwrap_err();
401 assert_eq!(
402 err,
403 TreeEntryError::DuplicateName {
404 name: b"foo".to_vec()
405 }
406 );
407 }
408
409 #[test]
410 fn rejects_bad_names_modes_and_ids() {
411 let cases: [(TreeEntry, TreeEntryError); 7] = [
412 (
413 entry(0o100644, &[b'x'; MAX_TREE_ENTRY_LEN + 1], 1),
414 TreeEntryError::NameTooLong {
415 name: vec![b'x'; MAX_TREE_ENTRY_LEN + 1],
416 max: MAX_TREE_ENTRY_LEN,
417 },
418 ),
419 (
420 entry(0o100644, b"", 1),
421 TreeEntryError::InvalidName {
422 name: Vec::new(),
423 reason: PathComponentError::Empty,
424 },
425 ),
426 (
427 entry(0o100644, b"a/b", 1),
428 TreeEntryError::InvalidName {
429 name: b"a/b".to_vec(),
430 reason: PathComponentError::ContainsSlash,
431 },
432 ),
433 (
434 entry(0o100644, b"..", 1),
435 TreeEntryError::InvalidName {
436 name: b"..".to_vec(),
437 reason: PathComponentError::DotDot,
438 },
439 ),
440 (
441 entry(MODE_TREE, b".GIT", 1),
442 TreeEntryError::InvalidName {
443 name: b".GIT".to_vec(),
444 reason: PathComponentError::DotGit,
445 },
446 ),
447 (
448 entry(0o100664, b"a", 1),
449 TreeEntryError::InvalidMode {
450 name: b"a".to_vec(),
451 mode: 0o100664,
452 },
453 ),
454 (
455 entry(0o100644, b"a", 0),
456 TreeEntryError::NullObjectId {
457 name: b"a".to_vec(),
458 },
459 ),
460 ];
461 for (input, expected) in cases {
462 assert_eq!(
463 canonical_tree_entries([input], HashAlgo::Sha1).unwrap_err(),
464 expected
465 );
466 }
467 }
468
469 #[test]
470 fn rejects_object_ids_of_the_other_hash_algorithm() {
471 let err = canonical_tree_entries([entry(0o100644, b"a", 1)], HashAlgo::Sha256).unwrap_err();
472 assert!(matches!(
473 err,
474 TreeEntryError::HashAlgoMismatch {
475 expected: HashAlgo::Sha256,
476 actual: HashAlgo::Sha1,
477 ..
478 }
479 ));
480 }
481
482 #[test]
483 fn commit_data_terminates_message_and_renders_signatures() {
484 let when = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
485 let sig = Signature::new("A U Thor", "a@example.com", when).unwrap();
486 let data = NewCommit::new(oid(1), sig.clone(), sig, "subject")
487 .with_parents([oid(2)])
488 .to_commit_data();
489 assert_eq!(data.message, "subject\n");
490 assert_eq!(data.parents, vec![oid(2)]);
491 assert_eq!(data.author, "A U Thor <a@example.com> 1700000000 +0000");
492 assert!(data.extra_headers.is_empty());
493 }
494}