1use crate::{HashKind, ObjectId, Result, error::invalid};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub enum EntryKind {
5 Blob,
6 Tree,
7 Commit,
8}
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct TreeEntry {
12 pub mode: u32,
13 pub name: Vec<u8>,
14 pub id: ObjectId,
15 pub kind: EntryKind,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct Tree {
20 pub entries: Vec<TreeEntry>,
21}
22
23impl Tree {
24 pub(crate) fn parse(data: &[u8], hash: HashKind, limit: usize) -> Result<Self> {
25 let hash_len = hash.bytes();
26 let mut cursor = 0;
27 let mut entries = Vec::new();
28 while cursor < data.len() {
29 if entries.len() == limit {
30 return Err(crate::GitError::LimitExceeded {
31 resource: "tree entries",
32 limit,
33 });
34 }
35 let space = data[cursor..]
36 .iter()
37 .position(|byte| *byte == b' ')
38 .ok_or_else(|| invalid("tree entry has no mode terminator"))?
39 + cursor;
40 let mode_text = std::str::from_utf8(&data[cursor..space])
41 .map_err(|_| invalid("tree mode is not ASCII"))?;
42 let mode =
43 u32::from_str_radix(mode_text, 8).map_err(|_| invalid("invalid tree mode"))?;
44 cursor = space + 1;
45 let nul = data[cursor..]
46 .iter()
47 .position(|byte| *byte == 0)
48 .ok_or_else(|| invalid("tree entry has no name terminator"))?
49 + cursor;
50 if nul == cursor || data[cursor..nul].contains(&b'/') {
51 return Err(invalid("invalid tree entry name"));
52 }
53 let name = data[cursor..nul].to_vec();
54 cursor = nul + 1;
55 let end = cursor
56 .checked_add(hash_len)
57 .ok_or_else(|| invalid("tree object identifier overflow"))?;
58 let bytes = data
59 .get(cursor..end)
60 .ok_or_else(|| invalid("truncated tree object identifier"))?;
61 let id = ObjectId::from_bytes(bytes)?;
62 cursor = end;
63 entries.push(TreeEntry {
64 mode,
65 name,
66 id,
67 kind: kind_from_mode(mode)?,
68 });
69 }
70 Ok(Self { entries })
71 }
72}
73
74fn kind_from_mode(mode: u32) -> Result<EntryKind> {
75 match mode {
76 0o040_000 => Ok(EntryKind::Tree),
77 0o160_000 => Ok(EntryKind::Commit),
78 0o100_644 | 0o100_755 | 0o120_000 => Ok(EntryKind::Blob),
79 _ => Err(invalid(format!("unsupported tree mode {mode:o}"))),
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::{EntryKind, Tree};
86 use crate::HashKind;
87
88 #[test]
89 fn parses_binary_tree() {
90 let mut input = b"100644 hello.txt\0".to_vec();
91 input.extend([7; 20]);
92 let tree = Tree::parse(&input, HashKind::Sha1, 10).unwrap();
93 assert_eq!(tree.entries[0].name, b"hello.txt");
94 assert_eq!(tree.entries[0].kind, EntryKind::Blob);
95 }
96
97 #[test]
98 fn rejects_invalid_and_limited_trees() {
99 assert!(Tree::parse(b"broken", HashKind::Sha1, 10).is_err());
100 let mut invalid_mode = b"100600 file\0".to_vec();
101 invalid_mode.extend([0; 20]);
102 assert!(Tree::parse(&invalid_mode, HashKind::Sha1, 10).is_err());
103 let mut slash = b"100644 a/b\0".to_vec();
104 slash.extend([0; 20]);
105 assert!(Tree::parse(&slash, HashKind::Sha1, 10).is_err());
106 let mut valid = b"100644 file\0".to_vec();
107 valid.extend([0; 20]);
108 assert!(Tree::parse(&valid, HashKind::Sha1, 0).is_err());
109 }
110}