Skip to main content

gix_odb/store_impls/dynamic/
find.rs

1use std::ops::Deref;
2
3use gix_pack::cache::DecodeEntry;
4
5use crate::store::{handle, load_index};
6
7pub(crate) mod error {
8    use crate::{loose, pack};
9
10    /// Returned by [`Handle::try_find()`][gix_pack::Find::try_find()]
11    #[derive(thiserror::Error, Debug)]
12    #[expect(missing_docs)]
13    pub enum Error {
14        #[error("An error occurred while obtaining an object from the loose object store")]
15        Loose(#[from] loose::find::Error),
16        #[error("An error occurred while obtaining an object from the packed object store")]
17        Pack(#[from] pack::data::decode::Error),
18        #[error(transparent)]
19        LoadIndex(#[from] crate::store::load_index::Error),
20        #[error(transparent)]
21        LoadPack(#[from] std::io::Error),
22        #[error(transparent)]
23        EntryType(#[from] gix_pack::data::entry::decode::Error),
24        #[error("Reached recursion limit of {} while resolving ref delta bases for {}", .max_depth, .id)]
25        DeltaBaseRecursionLimit {
26            /// the maximum recursion depth we encountered.
27            max_depth: usize,
28            /// The original object to lookup
29            id: gix_hash::ObjectId,
30        },
31        #[error("The base object {} could not be found but is required to decode {}", .base_id, .id)]
32        DeltaBaseMissing {
33            /// the id of the base object which failed to lookup
34            base_id: gix_hash::ObjectId,
35            /// The original object to lookup
36            id: gix_hash::ObjectId,
37        },
38        #[error("An error occurred when looking up a ref delta base object {} to decode {}", .base_id, .id)]
39        DeltaBaseLookup {
40            #[source]
41            err: Box<Self>,
42            /// the id of the base object which failed to lookup
43            base_id: gix_hash::ObjectId,
44            /// The original object to lookup
45            id: gix_hash::ObjectId,
46        },
47    }
48
49    #[derive(Copy, Clone)]
50    pub(crate) struct DeltaBaseRecursion<'a> {
51        pub depth: usize,
52        pub original_id: &'a gix_hash::oid,
53    }
54
55    impl<'a> DeltaBaseRecursion<'a> {
56        pub fn new(id: &'a gix_hash::oid) -> Self {
57            Self {
58                original_id: id,
59                depth: 0,
60            }
61        }
62        pub fn inc_depth(mut self) -> Self {
63            self.depth += 1;
64            self
65        }
66    }
67
68    #[cfg(test)]
69    mod tests {
70        use super::*;
71
72        #[test]
73        fn error_size() {
74            let actual = std::mem::size_of::<Error>();
75            assert!(actual <= 88, "{actual} <= 88: should not grow without us noticing");
76        }
77    }
78}
79pub use error::Error;
80
81use crate::store::types::PackId;
82
83impl<S> super::Handle<S>
84where
85    S: Deref<Target = super::Store> + Clone,
86{
87    fn try_find_cached_inner<'a, 'b>(
88        &'b self,
89        mut id: &'b gix_hash::oid,
90        buffer: &'a mut Vec<u8>,
91        inflate: &mut gix_zlib::Inflate,
92        pack_cache: &mut dyn DecodeEntry,
93        snapshot: &mut load_index::Snapshot,
94        recursion: Option<error::DeltaBaseRecursion<'_>>,
95    ) -> Result<Option<(gix_object::Data<'a>, Option<gix_pack::data::entry::Location>)>, Error> {
96        if let Some(r) = recursion {
97            if r.depth >= self.max_recursion_depth {
98                return Err(Error::DeltaBaseRecursionLimit {
99                    max_depth: self.max_recursion_depth,
100                    id: r.original_id.to_owned(),
101                });
102            }
103        } else if !self.ignore_replacements {
104            if let Ok(pos) = self
105                .store
106                .replacements
107                .binary_search_by(|(map_this, _)| map_this.as_ref().cmp(id))
108            {
109                id = self.store.replacements[pos].1.as_ref();
110            }
111        }
112
113        'outer: loop {
114            {
115                let marker = snapshot.marker;
116                for (idx, index) in snapshot.indices.iter_mut().enumerate() {
117                    if let Some(handle::index_lookup::Outcome {
118                        object_index: handle::IndexForObjectInPack { pack_id, pack_offset },
119                        index_file,
120                        pack: possibly_pack,
121                    }) = index.lookup(id)
122                    {
123                        let pack = match possibly_pack {
124                            Some(pack) => pack,
125                            None => match self.store.load_pack(pack_id, marker)? {
126                                Some(pack) => {
127                                    *possibly_pack = Some(pack);
128                                    possibly_pack.as_deref().expect("just put it in")
129                                }
130                                None => {
131                                    // The pack wasn't available anymore so we are supposed to try another round with a fresh index
132                                    match self.store.load_one_index(self.index_ctx(snapshot.marker))? {
133                                        Some(new_snapshot) => {
134                                            *snapshot = new_snapshot;
135                                            self.clear_cache();
136                                            continue 'outer;
137                                        }
138                                        None => {
139                                            // nothing new in the index, kind of unexpected to not have a pack but to also
140                                            // to have no new index yet. We set the new index before removing any slots, so
141                                            // this should be observable.
142                                            return Ok(None);
143                                        }
144                                    }
145                                }
146                            },
147                        };
148                        let entry = pack.entry(pack_offset)?;
149                        let header_size = entry.header_size();
150                        let res = pack.decode_entry(
151                            entry,
152                            buffer,
153                            inflate,
154                            &|id, _out| {
155                                let pack_offset = index_file.pack_offset_by_id(id)?;
156                                pack.entry(pack_offset)
157                                    .ok()
158                                    .map(gix_pack::data::decode::entry::ResolvedBase::InPack)
159                            },
160                            pack_cache,
161                        );
162                        let res = match res {
163                            Ok(r) => Ok((
164                                gix_object::Data {
165                                    kind: r.kind,
166                                    object_hash: pack.object_hash(),
167                                    data: buffer.as_slice(),
168                                },
169                                Some(gix_pack::data::entry::Location {
170                                    pack_id: pack.id,
171                                    pack_offset,
172                                    entry_size: r.compressed_size + header_size,
173                                }),
174                            )),
175                            Err(gix_pack::data::decode::Error::DeltaBaseUnresolved(base_id)) => {
176                                // Only with multi-pack indices it's allowed to jump to refer to other packs within this
177                                // multi-pack. Otherwise this would constitute a thin pack which is only allowed in transit.
178                                // However, if we somehow end up with that, we will resolve it safely, even though we could
179                                // avoid handling this case and error instead.
180
181                                // Since this is a special case, we just allocate here to make it work. It's an actual delta-ref object
182                                // which is sent by some servers that points to an object outside of the pack we are looking
183                                // at right now. With the complexities of loading packs, we go into recursion here. Git itself
184                                // doesn't do a cycle check, and we won't either but limit the recursive depth.
185                                // The whole ordeal isn't as efficient as it could be due to memory allocation and
186                                // later mem-copying when trying again.
187                                let mut buf = Vec::new();
188                                let obj_kind = self
189                                    .try_find_cached_inner(
190                                        &base_id,
191                                        &mut buf,
192                                        inflate,
193                                        pack_cache,
194                                        snapshot,
195                                        recursion
196                                            .map(error::DeltaBaseRecursion::inc_depth)
197                                            .or_else(|| error::DeltaBaseRecursion::new(id).into()),
198                                    )
199                                    .map_err(|err| Error::DeltaBaseLookup {
200                                        err: Box::new(err),
201                                        base_id,
202                                        id: id.to_owned(),
203                                    })?
204                                    .ok_or_else(|| Error::DeltaBaseMissing {
205                                        base_id,
206                                        id: id.to_owned(),
207                                    })?
208                                    .0
209                                    .kind;
210                                let handle::index_lookup::Outcome {
211                                    object_index:
212                                        handle::IndexForObjectInPack {
213                                            pack_id: _,
214                                            pack_offset,
215                                        },
216                                    index_file,
217                                    pack: possibly_pack,
218                                } = match snapshot.indices[idx].lookup(id) {
219                                    Some(res) => res,
220                                    None => {
221                                        let mut out = None;
222                                        for index in &mut snapshot.indices {
223                                            out = index.lookup(id);
224                                            if out.is_some() {
225                                                break;
226                                            }
227                                        }
228
229                                        out.unwrap_or_else(|| {
230                                           panic!("could not find object {id} in any index after looking up one of its base objects {base_id}" )
231                                       })
232                                    }
233                                };
234                                let pack = possibly_pack
235                                    .as_ref()
236                                    .expect("pack to still be available like just now");
237                                let entry = pack.entry(pack_offset)?;
238                                let header_size = entry.header_size();
239                                pack.decode_entry(
240                                    entry,
241                                    buffer,
242                                    inflate,
243                                    &|id, out| {
244                                        index_file
245                                            .pack_offset_by_id(id)
246                                            .and_then(|pack_offset| {
247                                                pack.entry(pack_offset)
248                                                    .ok()
249                                                    .map(gix_pack::data::decode::entry::ResolvedBase::InPack)
250                                            })
251                                            .or_else(|| {
252                                                (id == base_id).then(|| {
253                                                    out.resize(buf.len(), 0);
254                                                    out.copy_from_slice(buf.as_slice());
255                                                    gix_pack::data::decode::entry::ResolvedBase::OutOfPack {
256                                                        kind: obj_kind,
257                                                        end: out.len(),
258                                                    }
259                                                })
260                                            })
261                                    },
262                                    pack_cache,
263                                )
264                                .map(move |r| {
265                                    (
266                                        gix_object::Data {
267                                            kind: r.kind,
268                                            object_hash: pack.object_hash(),
269                                            data: buffer.as_slice(),
270                                        },
271                                        Some(gix_pack::data::entry::Location {
272                                            pack_id: pack.id,
273                                            pack_offset,
274                                            entry_size: r.compressed_size + header_size,
275                                        }),
276                                    )
277                                })
278                            }
279                            Err(err) => Err(err),
280                        }?;
281
282                        if idx != 0 {
283                            snapshot.indices.swap(0, idx);
284                        }
285                        return Ok(Some(res));
286                    }
287                }
288            }
289
290            for lodb in snapshot.loose_dbs.iter() {
291                // TODO: remove this double-lookup once the borrow checker allows it.
292                if lodb.contains(id) {
293                    return lodb
294                        .try_find(id, buffer)
295                        .map(|obj| obj.map(|obj| (obj, None)))
296                        .map_err(Into::into);
297                }
298            }
299
300            match self.store.load_one_index(self.index_ctx(snapshot.marker))? {
301                Some(new_snapshot) => {
302                    *snapshot = new_snapshot;
303                    self.clear_cache();
304                }
305                None => return Ok(None),
306            }
307        }
308    }
309
310    pub(crate) fn clear_cache(&self) {
311        self.packed_object_count.borrow_mut().take();
312    }
313}
314
315impl<S> gix_pack::Find for super::Handle<S>
316where
317    S: Deref<Target = super::Store> + Clone,
318{
319    // TODO: probably make this method fallible, but that would mean its own error type.
320    fn contains(&self, id: &gix_hash::oid) -> bool {
321        let mut snapshot = self.snapshot.borrow_mut();
322        loop {
323            for (idx, index) in snapshot.indices.iter().enumerate() {
324                if index.contains(id) {
325                    if idx != 0 {
326                        snapshot.indices.swap(0, idx);
327                    }
328                    return true;
329                }
330            }
331
332            for lodb in snapshot.loose_dbs.iter() {
333                if lodb.contains(id) {
334                    return true;
335                }
336            }
337
338            match self.store.load_one_index(self.index_ctx(snapshot.marker)) {
339                Ok(Some(new_snapshot)) => {
340                    *snapshot = new_snapshot;
341                    self.clear_cache();
342                }
343                Ok(None) => return false, // nothing more to load, or our refresh mode doesn't allow disk refreshes
344                Err(_) => return false, // something went wrong, nothing we can communicate here with this trait. TODO: Maybe that should change?
345            }
346        }
347    }
348
349    fn try_find_cached<'a>(
350        &self,
351        id: &gix_hash::oid,
352        buffer: &'a mut Vec<u8>,
353        pack_cache: &mut dyn DecodeEntry,
354    ) -> Result<Option<(gix_object::Data<'a>, Option<gix_pack::data::entry::Location>)>, gix_object::find::Error> {
355        let mut snapshot = self.snapshot.borrow_mut();
356        let mut inflate = self.inflate.borrow_mut();
357        self.try_find_cached_inner(id, buffer, &mut inflate, pack_cache, &mut snapshot, None)
358            .map_err(|err| Box::new(err) as _)
359    }
360
361    fn location_by_oid(&self, id: &gix_hash::oid, buf: &mut Vec<u8>) -> Option<gix_pack::data::entry::Location> {
362        assert!(
363            matches!(self.token.as_ref(), Some(handle::Mode::KeepDeletedPacksAvailable)),
364            "BUG: handle must be configured to `prevent_pack_unload()` before using this method"
365        );
366
367        assert!(
368            self.store_ref().replacements.is_empty() || self.ignore_replacements,
369            "Everything related to packing must not use replacements. These are not used here, but it should be turned off for good measure."
370        );
371
372        let mut snapshot = self.snapshot.borrow_mut();
373        let mut inflate = self.inflate.borrow_mut();
374        'outer: loop {
375            {
376                let marker = snapshot.marker;
377                for (idx, index) in snapshot.indices.iter_mut().enumerate() {
378                    if let Some(handle::index_lookup::Outcome {
379                        object_index: handle::IndexForObjectInPack { pack_id, pack_offset },
380                        index_file: _,
381                        pack: possibly_pack,
382                    }) = index.lookup(id)
383                    {
384                        let pack = match possibly_pack {
385                            Some(pack) => pack,
386                            None => match self.store.load_pack(pack_id, marker).ok()? {
387                                Some(pack) => {
388                                    *possibly_pack = Some(pack);
389                                    possibly_pack.as_deref().expect("just put it in")
390                                }
391                                None => {
392                                    // The pack wasn't available anymore so we are supposed to try another round with a fresh index
393                                    match self.store.load_one_index(self.index_ctx(snapshot.marker)).ok()? {
394                                        Some(new_snapshot) => {
395                                            *snapshot = new_snapshot;
396                                            self.clear_cache();
397                                            continue 'outer;
398                                        }
399                                        None => {
400                                            // nothing new in the index, kind of unexpected to not have a pack but to also
401                                            // to have no new index yet. We set the new index before removing any slots, so
402                                            // this should be observable.
403                                            return None;
404                                        }
405                                    }
406                                }
407                            },
408                        };
409                        let entry = pack.entry(pack_offset).ok()?;
410                        // This allocation is driven by on-disk pack metadata, so keep it aligned with
411                        // `gix_pack::data::File::with_alloc_limit_bytes()`.
412                        let size: usize = entry.decompressed_size.try_into().ok()?;
413                        if pack.alloc_limit_bytes.is_some_and(|limit| size > limit) {
414                            return None;
415                        }
416                        buf.resize(size, 0);
417                        assert_eq!(pack.id, pack_id.to_intrinsic_pack_id(), "both ids must always match");
418
419                        let res = pack
420                            .decompress_entry(&entry, &mut inflate, buf)
421                            .ok()
422                            .map(|entry_size_past_header| gix_pack::data::entry::Location {
423                                pack_id: pack.id,
424                                pack_offset,
425                                entry_size: entry.header_size() + entry_size_past_header,
426                            });
427
428                        if idx != 0 {
429                            snapshot.indices.swap(0, idx);
430                        }
431                        return res;
432                    }
433                }
434            }
435
436            {
437                let new_snapshot = self.store.load_one_index(self.index_ctx(snapshot.marker)).ok()??;
438                *snapshot = new_snapshot;
439                self.clear_cache();
440            }
441        }
442    }
443
444    fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(u64, gix_hash::ObjectId)>> {
445        assert!(
446            matches!(self.token.as_ref(), Some(handle::Mode::KeepDeletedPacksAvailable)),
447            "BUG: handle must be configured to `prevent_pack_unload()` before using this method"
448        );
449        let pack_id = PackId::from_intrinsic_pack_id(pack_id);
450        loop {
451            let snapshot = self.snapshot.borrow();
452            {
453                for index in &snapshot.indices {
454                    if let Some(iter) = index.iter(pack_id) {
455                        return Some(iter.map(|e| (e.pack_offset, e.oid)).collect());
456                    }
457                }
458            }
459
460            {
461                let new_snapshot = self.store.load_one_index(self.index_ctx(snapshot.marker)).ok()??;
462                drop(snapshot);
463                *self.snapshot.borrow_mut() = new_snapshot;
464            }
465        }
466    }
467
468    fn entry_by_location(&self, location: &gix_pack::data::entry::Location) -> Option<gix_pack::find::Entry> {
469        assert!(
470            matches!(self.token.as_ref(), Some(handle::Mode::KeepDeletedPacksAvailable)),
471            "BUG: handle must be configured to `prevent_pack_unload()` before using this method"
472        );
473        let pack_id = PackId::from_intrinsic_pack_id(location.pack_id);
474        let mut snapshot = self.snapshot.borrow_mut();
475        let marker = snapshot.marker;
476        loop {
477            {
478                for index in &mut snapshot.indices {
479                    if let Some(possibly_pack) = index.pack(pack_id) {
480                        let pack = match possibly_pack {
481                            Some(pack) => pack,
482                            None => {
483                                let pack = self.store.load_pack(pack_id, marker).ok()?.expect(
484                                "BUG: pack must exist from previous call to location_by_oid() and must not be unloaded",
485                            );
486                                *possibly_pack = Some(pack);
487                                possibly_pack.as_deref().expect("just put it in")
488                            }
489                        };
490                        return pack
491                            .entry_slice(location.entry_range(location.pack_offset))
492                            .map(|data| gix_pack::find::Entry {
493                                data: data.to_owned(),
494                                version: pack.version(),
495                            });
496                    }
497                }
498            }
499
500            snapshot.indices.insert(
501                0,
502                self.store
503                    .index_by_id(pack_id, marker)
504                    .expect("BUG: index must always be present, must not be unloaded or overwritten"),
505            );
506        }
507    }
508}
509
510impl<S> gix_object::Find for super::Handle<S>
511where
512    S: Deref<Target = super::Store> + Clone,
513    Self: gix_pack::Find,
514{
515    fn try_find<'a>(
516        &self,
517        id: &gix_hash::oid,
518        buffer: &'a mut Vec<u8>,
519    ) -> Result<Option<gix_object::Data<'a>>, gix_object::find::Error> {
520        gix_pack::Find::try_find(self, id, buffer).map(|t| t.map(|t| t.0))
521    }
522}
523
524impl<S> gix_object::FindHeader for super::Handle<S>
525where
526    S: Deref<Target = super::Store> + Clone,
527{
528    fn try_header(&self, id: &gix_hash::oid) -> Result<Option<gix_object::Header>, gix_object::find::Error> {
529        let mut snapshot = self.snapshot.borrow_mut();
530        let mut inflate = self.inflate.borrow_mut();
531        self.try_header_inner(id, &mut inflate, &mut snapshot, None)
532            .map(|maybe_header| {
533                maybe_header.map(|hdr| gix_object::Header {
534                    kind: hdr.kind(),
535                    size: hdr.size(),
536                })
537            })
538            .map_err(|err| Box::new(err) as _)
539    }
540}
541
542impl<S> gix_object::Exists for super::Handle<S>
543where
544    S: Deref<Target = super::Store> + Clone,
545    Self: gix_pack::Find,
546{
547    fn exists(&self, id: &gix_hash::oid) -> bool {
548        gix_pack::Find::contains(self, id)
549    }
550}