Skip to main content

git_zero_core/object/
parse.rs

1//! Borrowed structural views of commit, tree and annotated-tag bodies.
2use crate::{Error, ObjectFormat, ObjectId, ObjectKind, Result, names::cmp_tree_names};
3
4/// A commit's root tree and exact message bytes. This checks the root header
5/// and header/message separator, not identity semantics or graph reachability.
6#[derive(Clone, Copy, Debug)]
7pub struct Commit<'a> {
8    tree: ObjectId,
9    message: &'a [u8],
10    message_offset: usize,
11}
12
13impl<'a> Commit<'a> {
14    pub fn parse(data: &'a [u8]) -> Result<Self> {
15        Self::parse_with_format(data, ObjectFormat::Sha1)
16    }
17    pub fn parse_with_format(data: &'a [u8], format: ObjectFormat) -> Result<Self> {
18        let tree = Self::root_tree_with_format(data, format)?;
19        let separator = data
20            .windows(2)
21            .position(|bytes| bytes == b"\n\n")
22            .ok_or(Error::InvalidObject)?;
23        Ok(Self {
24            tree,
25            message: &data[separator + 2..],
26            message_offset: separator + 2,
27        })
28    }
29
30    /// Read just the first, canonical root-tree header.
31    pub fn root_tree(data: &[u8]) -> Result<ObjectId> {
32        Self::root_tree_with_format(data, ObjectFormat::Sha1)
33    }
34    pub fn root_tree_with_format(data: &[u8], format: ObjectFormat) -> Result<ObjectId> {
35        let end = 5 + format.hex_length();
36        let line = data.get(..end + 1).ok_or(Error::InvalidObject)?;
37        if !line.starts_with(b"tree ") || line[end] != b'\n' {
38            return Err(Error::InvalidObject);
39        }
40        ObjectId::from_hex(&line[5..end])
41    }
42
43    pub fn tree(&self) -> ObjectId {
44        self.tree
45    }
46    pub fn message(&self) -> &'a [u8] {
47        self.message
48    }
49    pub fn message_offset(&self) -> usize {
50        self.message_offset
51    }
52}
53
54/// One Git tree entry. Names are byte strings; filesystem-specific name limits
55/// and policies belong to the host. Modes retain Git's original octal values.
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub struct TreeEntry<'a> {
58    pub name: &'a [u8],
59    pub mode: u32,
60    pub id: ObjectId,
61}
62
63impl<'a> TreeEntry<'a> {
64    /// Read at a byte cursor, leaving it unchanged on error. This validates one
65    /// entry's mode, name and framing; use Tree::parse to check the whole order.
66    pub fn read(data: &'a [u8], offset: &mut usize) -> Result<Option<Self>> {
67        Self::read_with_format(data, offset, ObjectFormat::Sha1)
68    }
69    pub fn read_with_format(
70        data: &'a [u8],
71        offset: &mut usize,
72        format: ObjectFormat,
73    ) -> Result<Option<Self>> {
74        let tail = data.get(*offset..).ok_or(Error::InvalidObject)?;
75        if tail.is_empty() {
76            return Ok(None);
77        }
78        let space = tail
79            .iter()
80            .position(|&b| b == b' ')
81            .ok_or(Error::InvalidObject)?;
82        let mode = match &tail[..space] {
83            b"40000" => 0o040000,
84            b"100644" => 0o100644,
85            b"100755" => 0o100755,
86            b"120000" => 0o120000,
87            b"160000" => 0o160000,
88            _ => return Err(Error::InvalidObject),
89        };
90        let rest = &tail[space + 1..];
91        let nul = rest
92            .iter()
93            .position(|&b| b == 0)
94            .ok_or(Error::InvalidObject)?;
95        let name = &rest[..nul];
96        if name.is_empty() || name == b"." || name == b".." || name.contains(&b'/') {
97            return Err(Error::InvalidFileName);
98        }
99        let id_length = format.id_length();
100        let id = ObjectId::from_bytes(
101            format,
102            rest.get(nul + 1..nul + 1 + id_length)
103                .ok_or(Error::InvalidObject)?,
104        );
105        let id = id?;
106        *offset += space + 1 + nul + 1 + id_length;
107        Ok(Some(Self { name, mode, id }))
108    }
109}
110
111/// A structurally valid tree, including Git ordering and unique byte names.
112/// Validation uses constant storage. File/directory aliases can be separated
113/// by other names in Git order, so checking them may reread earlier entries.
114/// Every read, including these rescans, consumes an encoded-byte work budget.
115#[derive(Clone, Copy, Debug)]
116pub struct Tree<'a> {
117    data: &'a [u8],
118    format: ObjectFormat,
119}
120
121impl<'a> Tree<'a> {
122    /// Validate using a 64 MiB encoded-byte work budget. Hosts importing multiple
123    /// trees can share a different budget through parse_with_budget.
124    pub fn parse(data: &'a [u8]) -> Result<Self> {
125        Self::parse_with_format(data, ObjectFormat::Sha1)
126    }
127    pub fn parse_with_format(data: &'a [u8], format: ObjectFormat) -> Result<Self> {
128        Self::parse_with_budget_and_format(data, &mut (64 * 1024 * 1024), format)
129    }
130
131    /// Charge encoded bytes read, including duplicate-name rescans, against a
132    /// caller-owned budget. LimitExceeded stops before reading beyond it. Budget
133    /// already spent is retained on error; the tree remains borrowed/immutable.
134    pub fn parse_with_budget(data: &'a [u8], budget: &mut u64) -> Result<Self> {
135        Self::parse_with_budget_and_format(data, budget, ObjectFormat::Sha1)
136    }
137    pub fn parse_with_budget_and_format(
138        data: &'a [u8],
139        budget: &mut u64,
140        format: ObjectFormat,
141    ) -> Result<Self> {
142        let mut offset = 0;
143        let mut previous: Option<TreeEntry<'_>> = None;
144        let mut last_file_end = 0;
145        while let Some(entry) = read_budgeted(data, &mut offset, budget, format)? {
146            if let Some(previous) = previous {
147                if cmp_tree_names(
148                    previous.name,
149                    previous.mode == 0o040000,
150                    entry.name,
151                    entry.mode == 0o040000,
152                ) != core::cmp::Ordering::Less
153                {
154                    return Err(Error::UnsortedFiles);
155                }
156            }
157            if entry.mode == 0o040000 {
158                let mut earlier = 0;
159                // Directories cannot alias each other after the order check.
160                // Only the prefix containing earlier files needs a rescan.
161                while earlier < last_file_end {
162                    if read_budgeted(data, &mut earlier, budget, format)?
163                        .is_some_and(|other| other.name == entry.name)
164                    {
165                        return Err(Error::UnsortedFiles);
166                    }
167                }
168            } else {
169                last_file_end = offset;
170            }
171            previous = Some(entry);
172        }
173        Ok(Self { data, format })
174    }
175
176    pub fn entries(&self) -> TreeEntries<'a> {
177        TreeEntries {
178            data: self.data,
179            offset: 0,
180            format: self.format,
181        }
182    }
183}
184
185fn read_budgeted<'a>(
186    data: &'a [u8],
187    offset: &mut usize,
188    budget: &mut u64,
189    format: ObjectFormat,
190) -> Result<Option<TreeEntry<'a>>> {
191    if *offset == data.len() {
192        return Ok(None);
193    }
194    if *budget == 0 {
195        return Err(Error::LimitExceeded);
196    }
197    let start = *offset;
198    let available = usize::try_from(*budget).unwrap_or(usize::MAX);
199    let end = start.saturating_add(available).min(data.len());
200    let result = TreeEntry::read_with_format(&data[..end], offset, format);
201    if result.is_err() && end < data.len() {
202        return Err(Error::LimitExceeded);
203    }
204    *budget -= (*offset - start) as u64;
205    result
206}
207
208pub struct TreeEntries<'a> {
209    data: &'a [u8],
210    offset: usize,
211    format: ObjectFormat,
212}
213impl<'a> Iterator for TreeEntries<'a> {
214    type Item = TreeEntry<'a>;
215    fn next(&mut self) -> Option<Self::Item> {
216        // Only a fully validated, immutable Tree can construct this iterator.
217        TreeEntry::read_with_format(self.data, &mut self.offset, self.format)
218            .expect("validated tree")
219    }
220}
221
222/// The target and declared target kind of an annotated tag. Graph validation
223/// must additionally check that the actual target exists and has this kind.
224#[derive(Clone, Copy, Debug, Eq, PartialEq)]
225pub struct Tag {
226    pub target: ObjectId,
227    pub target_kind: ObjectKind,
228}
229
230impl Tag {
231    pub fn parse(data: &[u8]) -> Result<Self> {
232        let mut lines = data.split(|&byte| byte == b'\n');
233        let object = lines
234            .next()
235            .and_then(|line| line.strip_prefix(b"object "))
236            .ok_or(Error::InvalidObject)?;
237        let target = ObjectId::from_hex(object)?;
238        let target_kind = ObjectKind::from_tag_header(lines.next().ok_or(Error::InvalidObject)?)?;
239        let name = lines
240            .next()
241            .and_then(|line| line.strip_prefix(b"tag "))
242            .ok_or(Error::InvalidObject)?;
243        if name.is_empty() || !lines.any(|line| line.is_empty()) {
244            return Err(Error::InvalidObject);
245        }
246        Ok(Self {
247            target,
248            target_kind,
249        })
250    }
251}