Skip to main content

gix_object/tree/
ref_iter.rs

1use gix_error::ExnMessageResult;
2use std::ops::ControlFlow;
3
4use gix_error::ExnResult;
5
6use bstr::BStr;
7
8use crate::{TreeRef, TreeRefIter, tree, tree::EntryRef};
9
10/// Advance a path lookup by matching the next path component against `tree`.
11///
12/// `components` must yield the remaining path components to resolve, and `tree` must be the
13/// current object to search in.
14///
15/// The return value indicates how the caller should proceed:
16///
17/// - [`ControlFlow::Continue`] contains the object id of the matched entry when there are more
18///   components left to resolve. Callers should load that object and pass it back into a subsequent
19///   invocation.
20/// - [`ControlFlow::Break`]`(Some(entry))` contains the matched entry for the final component, and
21///   signals that lookup completed successfully.
22/// - [`ControlFlow::Break`]`(None)` signals that lookup cannot continue and should stop without a
23///   match. This happens if `tree` is not a tree object, if `components` is already exhausted, or if
24///   the next component is not present in `tree`.
25///
26/// Note that this behaviour is tuned to prefer to exhaust the entire chain of `components`, only the
27/// last component can yield a [`ControlFlow::Break`].
28pub fn next_entry<'a, I, P>(
29    components: &mut core::iter::Peekable<I>,
30    tree: crate::Data<'a>,
31) -> core::ops::ControlFlow<Option<EntryRef<'a>>, gix_hash::ObjectId>
32where
33    I: Iterator<Item = P>,
34    P: PartialEq<BStr>,
35{
36    if !tree.kind.is_tree() {
37        return ControlFlow::Break(None);
38    }
39
40    let Some(component) = components.next() else {
41        return ControlFlow::Break(None);
42    };
43
44    let Some(entry) = TreeRefIter::from_bytes(tree.data, tree.object_hash)
45        .filter_map(std::result::Result::ok)
46        .find(|entry| component.eq(entry.filename))
47    else {
48        return ControlFlow::Break(None);
49    };
50
51    if components.peek().is_none() {
52        ControlFlow::Break(Some(entry))
53    } else {
54        ControlFlow::Continue(entry.oid.to_owned())
55    }
56}
57
58impl<'a> TreeRefIter<'a> {
59    /// Instantiate an iterator from the given tree `data` and `object_hash`.
60    pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> TreeRefIter<'a> {
61        TreeRefIter { data, hash_kind }
62    }
63
64    /// Follow a sequence of `path` components starting from this instance, and look them up in `odb` one by one using `buffer`
65    /// until the last component is looked up and its tree entry is returned.
66    ///
67    /// # Performance Notes
68    ///
69    /// Searching tree entries is currently done in sequence, which allows the search to be allocation free. It would be possible
70    /// to reuse a vector and use a binary search instead, which might be able to improve performance over all.
71    /// However, a benchmark should be created first to have some data and see which trade-off to choose here.
72    pub fn lookup_entry<I, P>(
73        &self,
74        odb: impl crate::Find,
75        buffer: &'a mut Vec<u8>,
76        path: I,
77    ) -> ExnResult<Option<tree::Entry>>
78    where
79        I: IntoIterator<Item = P>,
80        P: PartialEq<BStr>,
81    {
82        buffer.clear();
83        buffer.extend_from_slice(self.data);
84
85        let mut iter = path.into_iter().peekable();
86        let mut data = crate::Data::new(buffer, crate::Kind::Tree, self.hash_kind);
87
88        loop {
89            data = match next_entry(&mut iter, data) {
90                ControlFlow::Continue(oid) => {
91                    let Some(next_tree) = odb.try_find(&oid, buffer)? else {
92                        break Ok(None);
93                    };
94                    next_tree
95                }
96                ControlFlow::Break(v) => break Ok(v.map(Into::into)),
97            }
98        }
99    }
100
101    /// Like [`Self::lookup_entry()`], but takes any [`AsRef<Path>`](`std::path::Path`) directly via `relative_path`,
102    /// a path relative to this tree.
103    /// `odb` and `buffer` are used to lookup intermediate trees.
104    ///
105    /// # Note
106    ///
107    /// If any path component contains illformed UTF-8 and thus can't be converted to bytes on platforms which can't do so natively,
108    /// the returned component will be empty which makes the lookup fail.
109    pub fn lookup_entry_by_path(
110        &self,
111        odb: impl crate::Find,
112        buffer: &'a mut Vec<u8>,
113        relative_path: impl AsRef<std::path::Path>,
114    ) -> ExnResult<Option<tree::Entry>> {
115        self.lookup_entry(
116            odb,
117            buffer,
118            relative_path
119                .as_ref()
120                .components()
121                .map(|c| c.as_os_str().as_encoded_bytes()),
122        )
123    }
124}
125
126impl<'a> TreeRef<'a> {
127    /// Deserialize a Tree from `data`, assuming `object_hash` to determine how the object ids are encoded in this particular tree.
128    pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> ExnMessageResult<TreeRef<'a>> {
129        decode::tree(data, hash_kind.len_in_bytes())
130    }
131
132    /// Find an entry named `name` knowing if the entry is a directory or not, using a binary search.
133    ///
134    /// Note that it's impossible to binary search by name alone as the sort order is special.
135    pub fn bisect_entry(&self, name: &BStr, is_dir: bool) -> Option<EntryRef<'a>> {
136        self.entries
137            .binary_search_by(|entry| tree::name_order(entry.filename, entry.mode.is_tree(), name, is_dir))
138            .ok()
139            .map(|idx| self.entries[idx])
140    }
141
142    /// Create an instance of the empty tree.
143    ///
144    /// It's particularly useful as static part of a program.
145    pub const fn empty() -> TreeRef<'static> {
146        TreeRef { entries: Vec::new() }
147    }
148}
149
150impl<'a> TreeRefIter<'a> {
151    /// Consume self and return all parsed entries.
152    pub fn entries(self) -> ExnMessageResult<Vec<EntryRef<'a>>> {
153        self.collect()
154    }
155
156    /// Return the offset in bytes that our data advanced from `buf`, the original buffer
157    /// to the beginning of the data of the tree.
158    ///
159    /// Then the tree-iteration can be resumed at the entry that would otherwise be returned next.
160    pub fn offset_to_next_entry(&self, buf: &[u8]) -> usize {
161        let before = (*buf).as_ptr();
162        let after = (*self.data).as_ptr();
163
164        debug_assert!(
165            before <= after,
166            "`TreeRefIter::offset_to_next_entry(): {after:?} <= {before:?}) violated"
167        );
168        (after as usize - before as usize) / std::mem::size_of::<u8>()
169    }
170}
171
172impl<'a> Iterator for TreeRefIter<'a> {
173    type Item = ExnMessageResult<EntryRef<'a>>;
174
175    fn next(&mut self) -> Option<Self::Item> {
176        if self.data.is_empty() {
177            return None;
178        }
179        match decode::fast_entry(self.data, self.hash_kind.len_in_bytes()) {
180            Some((data_left, entry)) => {
181                self.data = data_left;
182                Some(Ok(entry))
183            }
184            None => {
185                self.data = &[];
186                Some(Err(crate::decode::empty_error().into()))
187            }
188        }
189    }
190}
191
192impl<'a> TryFrom<&'a [u8]> for tree::EntryMode {
193    type Error = &'a [u8];
194
195    fn try_from(mode: &'a [u8]) -> std::result::Result<Self, Self::Error> {
196        tree::EntryMode::from_bytes(mode).ok_or(mode)
197    }
198}
199
200mod decode {
201    use bstr::ByteSlice;
202    use gix_error::ExnMessageResult;
203
204    use crate::{TreeRef, tree, tree::EntryRef};
205
206    pub fn fast_entry(i: &[u8], hash_len: usize) -> Option<(&[u8], EntryRef<'_>)> {
207        let (mode, i) = tree::EntryMode::extract_from_bytes(i)?;
208        let (filename, i) = i.split_at(i.find_byte(0)?);
209        let i = &i[1..];
210        let (oid, i) = match i.len() {
211            len if len < hash_len => return None,
212            _ => i.split_at(hash_len),
213        };
214        Some((
215            i,
216            EntryRef {
217                mode,
218                filename: filename.as_bstr(),
219                oid: gix_hash::oid::try_from_bytes(oid)
220                    .unwrap_or_else(|_| panic!("we counted exactly {hash_len} bytes")),
221            },
222        ))
223    }
224
225    pub fn tree(data: &[u8], hash_len: usize) -> ExnMessageResult<TreeRef<'_>> {
226        let mut i = data;
227
228        // Calculate an estimate of the amount of entries to reduce
229        // the amount of allocations necessary.
230        // Note that this assumes that we want speed over fitting Vecs, this is a trade-off.
231        const AVERAGE_FILENAME_LEN: usize = 24;
232        const AVERAGE_MODE_LEN: usize = 6;
233        const ENTRY_DELIMITER_LEN: usize = 2; // space + trailing zero
234        const AVERAGE_TREE_ENTRIES: usize = 16 * 2; // prevent overallocation beyond what's meaningful or what could be dangerous
235        let average_entry_len = ENTRY_DELIMITER_LEN + hash_len + AVERAGE_MODE_LEN + AVERAGE_FILENAME_LEN;
236        let upper_bound = i.len() / average_entry_len;
237        let mut out = Vec::with_capacity(upper_bound.min(AVERAGE_TREE_ENTRIES));
238
239        while !i.is_empty() {
240            let Some((rest, entry)) = fast_entry(i, hash_len) else {
241                return Err(crate::decode::empty_error().into());
242            };
243            i = rest;
244            out.push(entry);
245        }
246        Ok(TreeRef { entries: out })
247    }
248}