Skip to main content

weavatrix_git/
tree.rs

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}