use crate::{HashKind, ObjectId, Result, error::invalid};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EntryKind {
Blob,
Tree,
Commit,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeEntry {
pub mode: u32,
pub name: Vec<u8>,
pub id: ObjectId,
pub kind: EntryKind,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Tree {
pub entries: Vec<TreeEntry>,
}
impl Tree {
pub(crate) fn parse(data: &[u8], hash: HashKind, limit: usize) -> Result<Self> {
let hash_len = hash.bytes();
let mut cursor = 0;
let mut entries = Vec::new();
while cursor < data.len() {
if entries.len() == limit {
return Err(crate::GitError::LimitExceeded {
resource: "tree entries",
limit,
});
}
let space = data[cursor..]
.iter()
.position(|byte| *byte == b' ')
.ok_or_else(|| invalid("tree entry has no mode terminator"))?
+ cursor;
let mode_text = std::str::from_utf8(&data[cursor..space])
.map_err(|_| invalid("tree mode is not ASCII"))?;
let mode =
u32::from_str_radix(mode_text, 8).map_err(|_| invalid("invalid tree mode"))?;
cursor = space + 1;
let nul = data[cursor..]
.iter()
.position(|byte| *byte == 0)
.ok_or_else(|| invalid("tree entry has no name terminator"))?
+ cursor;
if nul == cursor || data[cursor..nul].contains(&b'/') {
return Err(invalid("invalid tree entry name"));
}
let name = data[cursor..nul].to_vec();
cursor = nul + 1;
let end = cursor
.checked_add(hash_len)
.ok_or_else(|| invalid("tree object identifier overflow"))?;
let bytes = data
.get(cursor..end)
.ok_or_else(|| invalid("truncated tree object identifier"))?;
let id = ObjectId::from_bytes(bytes)?;
cursor = end;
entries.push(TreeEntry {
mode,
name,
id,
kind: kind_from_mode(mode)?,
});
}
Ok(Self { entries })
}
}
fn kind_from_mode(mode: u32) -> Result<EntryKind> {
match mode {
0o040_000 => Ok(EntryKind::Tree),
0o160_000 => Ok(EntryKind::Commit),
0o100_644 | 0o100_755 | 0o120_000 => Ok(EntryKind::Blob),
_ => Err(invalid(format!("unsupported tree mode {mode:o}"))),
}
}
#[cfg(test)]
mod tests {
use super::{EntryKind, Tree};
use crate::HashKind;
#[test]
fn parses_binary_tree() {
let mut input = b"100644 hello.txt\0".to_vec();
input.extend([7; 20]);
let tree = Tree::parse(&input, HashKind::Sha1, 10).unwrap();
assert_eq!(tree.entries[0].name, b"hello.txt");
assert_eq!(tree.entries[0].kind, EntryKind::Blob);
}
#[test]
fn rejects_invalid_and_limited_trees() {
assert!(Tree::parse(b"broken", HashKind::Sha1, 10).is_err());
let mut invalid_mode = b"100600 file\0".to_vec();
invalid_mode.extend([0; 20]);
assert!(Tree::parse(&invalid_mode, HashKind::Sha1, 10).is_err());
let mut slash = b"100644 a/b\0".to_vec();
slash.extend([0; 20]);
assert!(Tree::parse(&slash, HashKind::Sha1, 10).is_err());
let mut valid = b"100644 file\0".to_vec();
valid.extend([0; 20]);
assert!(Tree::parse(&valid, HashKind::Sha1, 0).is_err());
}
}