Skip to main content

git_core/
tree.rs

1use crate::error::{GitError, Result};
2use crate::hash::GitHash;
3
4#[derive(Debug, Clone)]
5pub struct TreeEntry {
6    /// POSIX file mode (e.g. 0o100644 for regular file, 0o40000 for dir).
7    pub mode: u32,
8    pub name: String,
9    pub hash: GitHash,
10}
11
12impl TreeEntry {
13    pub fn is_directory(&self) -> bool {
14        self.mode == 0o40000
15    }
16
17    pub fn is_executable(&self) -> bool {
18        self.mode == 0o100_755
19    }
20
21    pub fn is_symlink(&self) -> bool {
22        self.mode == 0o120_000
23    }
24}
25
26#[derive(Debug, Clone)]
27pub struct TreeObject {
28    pub hash: GitHash,
29    pub entries: Vec<TreeEntry>,
30}
31
32impl TreeObject {
33    /// Parse raw tree object bytes (after the object header is stripped).
34    ///
35    /// Git tree binary format: repeated records of
36    /// `"<mode> <name>\0<20-byte SHA1>"`
37    pub fn parse(hash: GitHash, data: &[u8]) -> Result<Self> {
38        let mut entries = Vec::new();
39        let mut pos = 0;
40
41        while pos < data.len() {
42            let nul = data[pos..]
43                .iter()
44                .position(|&b| b == 0)
45                .ok_or_else(|| GitError::InvalidObject("tree entry missing NUL".into()))?;
46
47            let header = std::str::from_utf8(&data[pos..pos + nul]).map_err(|e| {
48                GitError::InvalidObject(format!("tree entry header not UTF-8: {e}"))
49            })?;
50
51            let (mode_str, name) = header
52                .split_once(' ')
53                .ok_or_else(|| GitError::InvalidObject("tree entry header missing space".into()))?;
54
55            let mode = u32::from_str_radix(mode_str, 8)
56                .map_err(|_| GitError::InvalidObject(format!("invalid mode: {mode_str:?}")))?;
57
58            pos += nul + 1;
59
60            if pos + 20 > data.len() {
61                return Err(GitError::InvalidObject(
62                    "tree entry truncated: no hash bytes".into(),
63                ));
64            }
65            let entry_hash = GitHash::from_bytes(&data[pos..pos + 20])?;
66            pos += 20;
67
68            entries.push(TreeEntry {
69                mode,
70                name: name.to_string(),
71                hash: entry_hash,
72            });
73        }
74
75        Ok(Self { hash, entries })
76    }
77}