use std::collections::HashSet;
use crate::change_id::ChangeId;
use crate::fsck_standalone::{tree_entry_mode_allowed, MAX_TREE_ENTRY_LEN};
use crate::ident::Signature;
use crate::objects::{tree_entry_cmp, CommitData, HashAlgo, ObjectId, TreeEntry};
use crate::path_validate::{audit_tree_entry_name, PathComponentError, ProtectOptions};
const MODE_TREE: u32 = 0o040000;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum TreeEntryError {
#[error("invalid tree entry name '{}': {reason}", String::from_utf8_lossy(.name))]
InvalidName {
name: Vec<u8>,
reason: PathComponentError,
},
#[error(
"tree entry name of {} bytes exceeds the {max}-byte limit",
.name.len()
)]
NameTooLong {
name: Vec<u8>,
max: usize,
},
#[error("tree entry '{}' has unsupported mode {mode:o}", String::from_utf8_lossy(.name))]
InvalidMode {
name: Vec<u8>,
mode: u32,
},
#[error("tree entry '{}' points at the null object id", String::from_utf8_lossy(.name))]
NullObjectId {
name: Vec<u8>,
},
#[error(
"tree entry '{}' has a {} object id in a {} repository",
String::from_utf8_lossy(.name),
.actual.name(),
.expected.name()
)]
HashAlgoMismatch {
name: Vec<u8>,
expected: HashAlgo,
actual: HashAlgo,
},
#[error("tree lists '{}' more than once", String::from_utf8_lossy(.name))]
DuplicateName {
name: Vec<u8>,
},
}
pub(crate) fn canonical_tree_entries(
entries: impl IntoIterator<Item = TreeEntry>,
algo: HashAlgo,
) -> Result<Vec<TreeEntry>, TreeEntryError> {
let mut entries: Vec<TreeEntry> = entries.into_iter().collect();
for entry in &entries {
validate_tree_entry(entry, algo)?;
}
entries
.sort_by(|a, b| tree_entry_cmp(&a.name, a.mode == MODE_TREE, &b.name, b.mode == MODE_TREE));
let mut seen = HashSet::with_capacity(entries.len());
if let Some(dup) = entries.iter().find(|e| !seen.insert(e.name.as_slice())) {
return Err(TreeEntryError::DuplicateName {
name: dup.name.clone(),
});
}
Ok(entries)
}
fn validate_tree_entry(entry: &TreeEntry, algo: HashAlgo) -> Result<(), TreeEntryError> {
audit_tree_entry_name(&entry.name, ProtectOptions::fsck())
.first_error()
.map_err(|reason| TreeEntryError::InvalidName {
name: entry.name.clone(),
reason,
})?;
if entry.name.len() > MAX_TREE_ENTRY_LEN {
return Err(TreeEntryError::NameTooLong {
name: entry.name.clone(),
max: MAX_TREE_ENTRY_LEN,
});
}
if !tree_entry_mode_allowed(entry.mode) {
return Err(TreeEntryError::InvalidMode {
name: entry.name.clone(),
mode: entry.mode,
});
}
if entry.oid.algo() != algo {
return Err(TreeEntryError::HashAlgoMismatch {
name: entry.name.clone(),
expected: algo,
actual: entry.oid.algo(),
});
}
if entry.oid.is_zero() {
return Err(TreeEntryError::NullObjectId {
name: entry.name.clone(),
});
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewCommit {
tree: ObjectId,
parents: Vec<ObjectId>,
author: Signature,
committer: Signature,
message: String,
change_id: Option<ChangeId>,
}
impl NewCommit {
#[must_use]
pub fn new(
tree: ObjectId,
author: Signature,
committer: Signature,
message: impl Into<String>,
) -> Self {
Self {
tree,
parents: Vec::new(),
author,
committer,
message: message.into(),
change_id: None,
}
}
#[must_use]
pub fn with_parents(mut self, parents: impl IntoIterator<Item = ObjectId>) -> Self {
self.parents = parents.into_iter().collect();
self
}
#[must_use]
pub fn with_change_id(mut self, id: ChangeId) -> Self {
self.change_id = Some(id);
self
}
pub(crate) fn change_id(&self) -> Option<&ChangeId> {
self.change_id.as_ref()
}
pub(crate) fn tree_and_parents(&self) -> (&ObjectId, &[ObjectId]) {
(&self.tree, &self.parents)
}
pub(crate) fn signatures(&self) -> [&Signature; 2] {
[&self.author, &self.committer]
}
pub(crate) fn to_commit_data(&self) -> CommitData {
let mut message = self.message.clone();
if !message.is_empty() && !message.ends_with('\n') {
message.push('\n');
}
CommitData {
tree: self.tree,
parents: self.parents.clone(),
author: self.author.header_line(),
committer: self.committer.header_line(),
author_raw: Vec::new(),
committer_raw: Vec::new(),
encoding: None,
message,
raw_message: None,
extra_headers: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objects::HashAlgo;
use time::OffsetDateTime;
fn oid(byte: u8) -> ObjectId {
ObjectId::from_bytes(&[byte; 20]).unwrap()
}
fn entry(mode: u32, name: &[u8], byte: u8) -> TreeEntry {
TreeEntry {
mode,
name: name.to_vec(),
oid: oid(byte),
}
}
#[test]
fn sorts_into_git_tree_order() {
let sorted = canonical_tree_entries(
[
entry(0o100644, b"foo.c", 1),
entry(MODE_TREE, b"foo", 2),
entry(0o100644, b"foo-bar", 3),
entry(0o100644, b"a", 4),
],
HashAlgo::Sha1,
)
.unwrap();
let names: Vec<&[u8]> = sorted.iter().map(|e| e.name.as_slice()).collect();
assert_eq!(
names,
[&b"a"[..], &b"foo-bar"[..], &b"foo.c"[..], &b"foo"[..]]
);
}
#[test]
fn rejects_file_and_directory_with_the_same_name() {
let err = canonical_tree_entries(
[
entry(0o100644, b"foo", 1),
entry(0o100644, b"foo.c", 2),
entry(MODE_TREE, b"foo", 3),
],
HashAlgo::Sha1,
)
.unwrap_err();
assert_eq!(
err,
TreeEntryError::DuplicateName {
name: b"foo".to_vec()
}
);
}
#[test]
fn rejects_bad_names_modes_and_ids() {
let cases: [(TreeEntry, TreeEntryError); 7] = [
(
entry(0o100644, &[b'x'; MAX_TREE_ENTRY_LEN + 1], 1),
TreeEntryError::NameTooLong {
name: vec![b'x'; MAX_TREE_ENTRY_LEN + 1],
max: MAX_TREE_ENTRY_LEN,
},
),
(
entry(0o100644, b"", 1),
TreeEntryError::InvalidName {
name: Vec::new(),
reason: PathComponentError::Empty,
},
),
(
entry(0o100644, b"a/b", 1),
TreeEntryError::InvalidName {
name: b"a/b".to_vec(),
reason: PathComponentError::ContainsSlash,
},
),
(
entry(0o100644, b"..", 1),
TreeEntryError::InvalidName {
name: b"..".to_vec(),
reason: PathComponentError::DotDot,
},
),
(
entry(MODE_TREE, b".GIT", 1),
TreeEntryError::InvalidName {
name: b".GIT".to_vec(),
reason: PathComponentError::DotGit,
},
),
(
entry(0o100664, b"a", 1),
TreeEntryError::InvalidMode {
name: b"a".to_vec(),
mode: 0o100664,
},
),
(
entry(0o100644, b"a", 0),
TreeEntryError::NullObjectId {
name: b"a".to_vec(),
},
),
];
for (input, expected) in cases {
assert_eq!(
canonical_tree_entries([input], HashAlgo::Sha1).unwrap_err(),
expected
);
}
}
#[test]
fn rejects_object_ids_of_the_other_hash_algorithm() {
let err = canonical_tree_entries([entry(0o100644, b"a", 1)], HashAlgo::Sha256).unwrap_err();
assert!(matches!(
err,
TreeEntryError::HashAlgoMismatch {
expected: HashAlgo::Sha256,
actual: HashAlgo::Sha1,
..
}
));
}
#[test]
fn commit_data_terminates_message_and_renders_signatures() {
let when = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
let sig = Signature::new("A U Thor", "a@example.com", when).unwrap();
let data = NewCommit::new(oid(1), sig.clone(), sig, "subject")
.with_parents([oid(2)])
.to_commit_data();
assert_eq!(data.message, "subject\n");
assert_eq!(data.parents, vec![oid(2)]);
assert_eq!(data.author, "A U Thor <a@example.com> 1700000000 +0000");
assert!(data.extra_headers.is_empty());
}
}