Skip to main content

grit_lib/odb/store/
packs.rs

1//! Read-only pack-file object backend ([`PackedObjects`]).
2//!
3//! All pack cache and index reads run under the owning [`PackStore`](crate::pack_store::PackStore)
4//! context via [`PackStore::with_context`](crate::pack_store::PackStore::with_context).
5
6use std::collections::HashSet;
7use std::io::{self, Cursor, Read};
8use std::ops::ControlFlow;
9use std::path::Path;
10use std::sync::Arc;
11
12use flate2::{Decompress, FlushDecompress, Status};
13
14use crate::error::{Error, Result};
15use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
16use crate::pack::{self, read_local_pack_indexes_cached, reprepare_pack_directory_on_miss};
17use crate::pack_index::PackIndex;
18use crate::pack_store::PackStore;
19
20use super::{ObjectStore, ObjectStream};
21
22/// Which local pack indexes participate in filtered membership checks.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct PackFilter {
25    /// When false, skip indexes with a sibling `.promisor` marker.
26    pub include_promisor: bool,
27    /// When false, skip indexes with a sibling `.mtimes` (cruft) marker.
28    pub include_cruft: bool,
29}
30
31impl Default for PackFilter {
32    fn default() -> Self {
33        Self {
34            include_promisor: false,
35            include_cruft: true,
36        }
37    }
38}
39
40/// Pack-index-backed read-only [`ObjectStore`] for one `objects/` directory.
41pub struct PackedObjects {
42    store: Arc<PackStore>,
43}
44
45impl std::fmt::Debug for PackedObjects {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("PackedObjects")
48            .field("objects_dir", &self.store.objects_dir())
49            .finish()
50    }
51}
52
53impl PackedObjects {
54    /// Open pack reads for `store`'s `objects/` directory.
55    #[must_use]
56    pub fn new(store: Arc<PackStore>) -> Self {
57        Self { store }
58    }
59
60    /// Shared pack cache handle (same as used for reads).
61    #[must_use]
62    pub fn pack_store(&self) -> &Arc<PackStore> {
63        &self.store
64    }
65
66    /// Object ids listed in a pack index file (for ingest paths outside the live ODB cache).
67    ///
68    /// # Errors
69    ///
70    /// Propagates pack index parse/verify failures from [`pack::read_pack_index`].
71    pub fn object_ids_from_index_path(idx_path: &Path) -> Result<HashSet<ObjectId>> {
72        let idx = pack::read_pack_index(idx_path)?;
73        let mut out = HashSet::new();
74        for entry in idx.iter() {
75            if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
76                out.insert(oid);
77            }
78        }
79        Ok(out)
80    }
81
82    /// Whether `oid` appears in any pack index matching `filter`.
83    ///
84    /// Used for local materialization checks (promisor/cruft exclusion); not part of
85    /// [`ObjectStore::contains`], which ignores promisor packs.
86    ///
87    /// # Errors
88    ///
89    /// Propagates pack directory enumeration failures.
90    pub fn contains_filtered(&self, oid: &ObjectId, filter: PackFilter) -> Result<bool> {
91        self.with_pack(|objects_dir| {
92            for idx in filtered_indexes(objects_dir, filter)? {
93                if idx.contains(oid)? {
94                    return Ok(true);
95                }
96            }
97            Ok(false)
98        })
99    }
100
101    /// Invoke `f` for each object id stored in a promisor-marked pack index.
102    ///
103    /// # Errors
104    ///
105    /// Propagates pack index enumeration failures.
106    pub fn for_each_promisor_object(
107        &self,
108        f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
109    ) -> Result<()> {
110        self.with_pack(|objects_dir| {
111            let indexes = read_local_pack_indexes_cached(objects_dir)?;
112            let mut seen = HashSet::new();
113            for idx in indexes {
114                if !idx.is_promisor {
115                    continue;
116                }
117                for entry in idx.iter() {
118                    let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
119                        continue;
120                    };
121                    if !seen.insert(oid) {
122                        continue;
123                    }
124                    if f(&oid).is_break() {
125                        return Ok(());
126                    }
127                }
128            }
129            Ok(())
130        })
131    }
132
133    /// Collect object ids from pack indexes that have a sibling `.keep` file.
134    ///
135    /// # Errors
136    ///
137    /// Propagates pack directory or index read failures.
138    pub fn kept_pack_object_ids(&self) -> Result<HashSet<ObjectId>> {
139        self.with_pack(collect_kept_pack_object_ids)
140    }
141
142    /// Invoke `f` for each object id in a local pack index without a sibling `.keep` file.
143    ///
144    /// Used by repack when retaining unreachable storage from packs that will be removed.
145    ///
146    /// # Errors
147    ///
148    /// Propagates pack index enumeration failures.
149    pub fn for_each_removable_pack_object(
150        &self,
151        f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
152    ) -> Result<()> {
153        self.with_pack(|objects_dir| for_each_removable_pack_object_ids(objects_dir, f))
154    }
155
156    /// Touch the mtime of the pack file holding `oid` (Git pack freshen), when not cruft.
157    ///
158    /// # Errors
159    ///
160    /// Propagates I/O failures from pack listing or utime.
161    pub fn freshen_pack_containing(&self, oid: &ObjectId) -> Result<bool> {
162        self.with_pack(|objects_dir| {
163            let indexes = read_local_pack_indexes_cached(objects_dir)?;
164            for idx in &indexes {
165                if !idx.contains(oid)? {
166                    continue;
167                }
168                if idx.is_cruft {
169                    return Ok(false);
170                }
171                return Ok(touch_pack_mtime(&idx.pack_path));
172            }
173            Ok(false)
174        })
175    }
176
177    fn with_pack<R>(&self, f: impl FnOnce(&std::path::Path) -> Result<R>) -> Result<R> {
178        if PackStore::read_context_matches_objects_dir(self.store.objects_dir()) {
179            return f(self.store.objects_dir());
180        }
181        PackStore::with_context(Arc::clone(&self.store), || f(self.store.objects_dir()))
182    }
183
184    fn hash_algo_from_indexes(&self) -> HashAlgo {
185        self.with_pack(|objects_dir| {
186            let Ok(indexes) = read_local_pack_indexes_cached(objects_dir) else {
187                return Ok(HashAlgo::Sha1);
188            };
189            Ok(indexes
190                .first()
191                .and_then(|idx| HashAlgo::from_len(idx.hash_bytes()))
192                .unwrap_or(HashAlgo::Sha1))
193        })
194        .unwrap_or(HashAlgo::Sha1)
195    }
196
197    fn read_from_packs(&self, oid: &ObjectId) -> Result<Option<Object>> {
198        self.with_pack(
199            |objects_dir| match pack::read_object_from_packs(objects_dir, oid) {
200                Ok(obj) => Ok(Some(obj)),
201                Err(Error::ObjectNotFound(_)) => Ok(None),
202                Err(err) => Err(err),
203            },
204        )
205    }
206
207    fn read_from_packs_into(
208        &self,
209        oid: &ObjectId,
210        buf: &mut Vec<u8>,
211    ) -> Result<Option<ObjectKind>> {
212        self.with_pack(|objects_dir| {
213            match pack::read_object_from_packs_into(objects_dir, oid, buf) {
214                Ok(kind) => Ok(Some(kind)),
215                Err(Error::ObjectNotFound(_)) => Ok(None),
216                Err(err) => Err(err),
217            }
218        })
219    }
220
221    fn read_info_from_packs(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
222        self.with_pack(
223            |objects_dir| match pack::read_object_info_from_packs(objects_dir, oid) {
224                Ok(info) => Ok(Some(info)),
225                Err(Error::ObjectNotFound(_)) => Ok(None),
226                Err(err) => Err(err),
227            },
228        )
229    }
230}
231
232impl ObjectStore for PackedObjects {
233    fn hash_algo(&self) -> HashAlgo {
234        self.hash_algo_from_indexes()
235    }
236
237    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
238        self.read_from_packs(oid)
239    }
240
241    fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
242        self.read_from_packs_into(oid, buf)
243    }
244
245    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
246        self.read_info_from_packs(oid)
247    }
248
249    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
250        self.with_pack(|dir| {
251            let mut entry = pack::locate_packed_whole_object(dir, oid, true)?;
252            if matches!(entry, pack::PackedEntry::Absent) && reprepare_pack_directory_on_miss(dir)?
253            {
254                entry = pack::locate_packed_whole_object(dir, oid, true)?;
255            }
256            let pack::PackedEntry::Whole(whole) = entry else {
257                return stream_from_buffered_read(self, oid);
258            };
259            Ok(Some(ObjectStream {
260                kind: whole.kind,
261                size: whole.size,
262                reader: Box::new(PackPayloadStream::new(whole)),
263            }))
264        })
265    }
266
267    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
268        self.with_pack(|objects_dir| {
269            let indexes = read_local_pack_indexes_cached(objects_dir)?;
270            let mut seen = HashSet::new();
271            for idx in &indexes {
272                for entry in idx.iter() {
273                    let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
274                        continue;
275                    };
276                    if !seen.insert(oid) {
277                        continue;
278                    }
279                    if f(&oid).is_break() {
280                        return Ok(());
281                    }
282                }
283            }
284            Ok(())
285        })
286    }
287
288    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
289        if prefix.len() > self.hash_algo().hex_len() {
290            return Ok(());
291        }
292        let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
293        self.with_pack(|objects_dir| {
294            let indexes = read_local_pack_indexes_cached(objects_dir)?;
295            let mut seen = HashSet::new();
296            let mut count = 0usize;
297            for idx in &indexes {
298                collect_prefix_from_index(idx, &prefix, limit, &mut count, out, &mut seen)?;
299                if limit != 0 && count >= limit {
300                    break;
301                }
302            }
303            Ok(())
304        })
305    }
306
307    fn refresh(&self) -> Result<bool> {
308        self.with_pack(reprepare_pack_directory_on_miss)
309    }
310}
311
312fn for_each_removable_pack_object_ids(
313    objects_dir: &Path,
314    f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
315) -> Result<()> {
316    let pack_dir = objects_dir.join("pack");
317    let indexes = read_local_pack_indexes_cached(objects_dir)?;
318    let mut seen = HashSet::new();
319    for idx in indexes {
320        if pack_index_has_keep_marker(&pack_dir, &idx.pack_path) {
321            continue;
322        }
323        let hash_bytes = idx.hash_bytes();
324        for entry in idx.iter() {
325            if entry.oid().len() != hash_bytes {
326                continue;
327            }
328            let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
329                continue;
330            };
331            if !seen.insert(oid) {
332                continue;
333            }
334            if f(&oid).is_break() {
335                return Ok(());
336            }
337        }
338    }
339    Ok(())
340}
341
342fn pack_index_has_keep_marker(pack_dir: &Path, pack_path: &Path) -> bool {
343    let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
344        return false;
345    };
346    pack_dir.join(format!("{stem}.keep")).is_file()
347}
348
349fn collect_kept_pack_object_ids(objects_dir: &Path) -> Result<HashSet<ObjectId>> {
350    let pack_dir = objects_dir.join("pack");
351    let mut kept = HashSet::new();
352    if !pack_dir.is_dir() {
353        return Ok(kept);
354    }
355    for entry in std::fs::read_dir(&pack_dir).map_err(Error::Io)? {
356        let entry = entry.map_err(Error::Io)?;
357        let path = entry.path();
358        if path.extension().is_some_and(|ext| ext == "keep") {
359            let idx_path = path.with_extension("idx");
360            if idx_path.is_file() {
361                if let Ok(oids) = pack::read_idx_object_ids(&idx_path) {
362                    kept.extend(oids);
363                }
364            }
365        }
366    }
367    Ok(kept)
368}
369
370fn filtered_indexes(
371    objects_dir: &std::path::Path,
372    filter: PackFilter,
373) -> Result<Vec<Arc<PackIndex>>> {
374    let indexes = read_local_pack_indexes_cached(objects_dir)?;
375    Ok(indexes
376        .into_iter()
377        .filter(|idx| {
378            (filter.include_promisor || !idx.is_promisor) && (filter.include_cruft || !idx.is_cruft)
379        })
380        .collect())
381}
382
383fn collect_prefix_from_index(
384    idx: &PackIndex,
385    prefix: &ObjectIdPrefix,
386    limit: usize,
387    count: &mut usize,
388    out: &mut Vec<ObjectId>,
389    seen: &mut HashSet<ObjectId>,
390) -> Result<()> {
391    let lo = lower_bound_oid(idx, prefix.lowest().as_bytes());
392    let hi = upper_bound_oid(idx, prefix.highest().as_bytes());
393    for i in lo..hi {
394        let digest = idx.oid_at(i);
395        if !prefix.matches_digest(digest) {
396            continue;
397        }
398        let Ok(oid) = ObjectId::from_bytes(digest) else {
399            continue;
400        };
401        if !seen.insert(oid) {
402            continue;
403        }
404        out.push(oid);
405        *count += 1;
406        if limit != 0 && *count >= limit {
407            break;
408        }
409    }
410    Ok(())
411}
412
413fn lower_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
414    let mut lo = 0usize;
415    let mut hi = idx.len();
416    while lo < hi {
417        let mid = lo + (hi - lo) / 2;
418        if idx.oid_at(mid) < needle {
419            lo = mid + 1;
420        } else {
421            hi = mid;
422        }
423    }
424    lo
425}
426
427fn upper_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
428    let mut lo = 0usize;
429    let mut hi = idx.len();
430    while lo < hi {
431        let mid = lo + (hi - lo) / 2;
432        if idx.oid_at(mid) <= needle {
433            lo = mid + 1;
434        } else {
435            hi = mid;
436        }
437    }
438    lo
439}
440
441/// Freshen `pack_path` (like rewriting objects it already holds) so age-based pruning
442/// treats its objects as new. Returns `false` when the times cannot be set.
443pub(crate) fn touch_pack_mtime(pack_path: &std::path::Path) -> bool {
444    let now = filetime::FileTime::now();
445    if filetime::set_file_times(pack_path, now, now).is_err() {
446        return false;
447    }
448    let touched = std::time::UNIX_EPOCH
449        + std::time::Duration::new(now.unix_seconds() as u64, now.nanoseconds());
450    pack::refresh_pack_bytes_signature(pack_path, touched);
451    true
452}
453
454/// Inflates a whole (non-delta) pack entry straight out of the mapped pack.
455///
456/// Yields exactly the declared payload size, then requires the zlib stream to end there, so
457/// truncated, oversized or checksum-corrupt entries surface as read errors.
458struct PackPayloadStream {
459    whole: pack::PackedWholeObject,
460    dec: Decompress,
461    pos: usize,
462    remaining: u64,
463    stream_ended: bool,
464}
465
466impl PackPayloadStream {
467    fn new(whole: pack::PackedWholeObject) -> Self {
468        Self {
469            pos: whole.zlib_offset,
470            remaining: whole.size,
471            whole,
472            dec: Decompress::new(true),
473            stream_ended: false,
474        }
475    }
476
477    /// Inflate into `out`, returning the bytes produced.
478    fn inflate(&mut self, out: &mut [u8]) -> io::Result<usize> {
479        let input = &self.whole.pack[self.pos..self.whole.data_end];
480        let before_in = self.dec.total_in();
481        let before_out = self.dec.total_out();
482        let status = self
483            .dec
484            .decompress(input, out, FlushDecompress::None)
485            .map_err(|e| corrupt_pack_stream(&e.to_string()))?;
486        let consumed = (self.dec.total_in() - before_in) as usize;
487        let produced = (self.dec.total_out() - before_out) as usize;
488        self.pos += consumed;
489        if status == Status::StreamEnd {
490            self.stream_ended = true;
491        } else if consumed == 0 && produced == 0 {
492            return Err(corrupt_pack_stream("zlib stream truncated"));
493        }
494        Ok(produced)
495    }
496}
497
498fn corrupt_pack_stream(detail: &str) -> io::Error {
499    io::Error::new(
500        io::ErrorKind::InvalidData,
501        format!("corrupt packed object: {detail}"),
502    )
503}
504
505impl Read for PackPayloadStream {
506    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
507        if self.remaining == 0 {
508            let mut excess = [0u8; 1];
509            while !self.stream_ended {
510                if self.inflate(&mut excess)? > 0 {
511                    return Err(corrupt_pack_stream("more data than declared size"));
512                }
513            }
514            return Ok(0);
515        }
516        if buf.is_empty() {
517            return Ok(0);
518        }
519        let max = usize::try_from(self.remaining).map_or(buf.len(), |r| r.min(buf.len()));
520        loop {
521            if self.stream_ended {
522                return Err(corrupt_pack_stream("less data than declared size"));
523            }
524            let produced = self.inflate(&mut buf[..max])?;
525            if produced > 0 {
526                self.remaining -= produced as u64;
527                return Ok(produced);
528            }
529        }
530    }
531}
532
533fn stream_from_buffered_read(
534    store: &PackedObjects,
535    oid: &ObjectId,
536) -> Result<Option<ObjectStream<'static>>> {
537    let Some(obj) = store.read(oid)? else {
538        return Ok(None);
539    };
540    let size = u64::try_from(obj.data.len())
541        .map_err(|_| Error::CorruptObject(format!("object size overflow for {}", oid.to_hex())))?;
542    Ok(Some(ObjectStream {
543        kind: obj.kind,
544        size,
545        reader: Box::new(Cursor::new(obj.data)),
546    }))
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use flate2::write::ZlibEncoder;
553    use flate2::Compression;
554    use std::io::Write;
555
556    use crate::pack_map::PackData;
557
558    const TRAILER: usize = 20;
559
560    fn zlib(data: &[u8]) -> Vec<u8> {
561        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
562        enc.write_all(data).expect("deflate");
563        enc.finish().expect("deflate")
564    }
565
566    /// A fake pack holding one whole entry: some leading bytes, the zlib stream, a trailer.
567    fn whole(zlib_stream: &[u8], declared_size: u64) -> pack::PackedWholeObject {
568        let mut bytes = b"PACK\0\0\0\x02\0\0\0\x01\xb0".to_vec();
569        let zlib_offset = bytes.len();
570        bytes.extend_from_slice(zlib_stream);
571        let data_end = bytes.len();
572        bytes.extend_from_slice(&[0u8; TRAILER]);
573        pack::PackedWholeObject {
574            pack: PackData::from_owned(bytes),
575            zlib_offset,
576            data_end,
577            kind: ObjectKind::Blob,
578            size: declared_size,
579        }
580    }
581
582    fn read_all(whole: pack::PackedWholeObject) -> io::Result<Vec<u8>> {
583        let mut stream = PackPayloadStream::new(whole);
584        let mut out = Vec::new();
585        let mut chunk = [0u8; 1000];
586        loop {
587            match stream.read(&mut chunk)? {
588                0 => return Ok(out),
589                n => out.extend_from_slice(&chunk[..n]),
590            }
591        }
592    }
593
594    fn data() -> Vec<u8> {
595        (0..200_000u32)
596            .flat_map(|i| (i % 251).to_le_bytes())
597            .collect()
598    }
599
600    #[test]
601    fn pack_payload_stream_yields_the_whole_payload() {
602        let data = data();
603        let out = read_all(whole(&zlib(&data), data.len() as u64)).expect("stream");
604        assert!(out == data);
605    }
606
607    #[test]
608    fn pack_payload_stream_rejects_truncated_entry() {
609        let data = data();
610        let stream = zlib(&data);
611        let err =
612            read_all(whole(&stream[..stream.len() / 2], data.len() as u64)).expect_err("truncated");
613        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
614    }
615
616    #[test]
617    fn pack_payload_stream_rejects_size_mismatch_both_ways() {
618        let data = data();
619        let stream = zlib(&data);
620        let err = read_all(whole(&stream, data.len() as u64 + 1)).expect_err("short");
621        assert!(err.to_string().contains("less data"), "{err}");
622        let err = read_all(whole(&stream, data.len() as u64 - 1)).expect_err("long");
623        assert!(err.to_string().contains("more data"), "{err}");
624    }
625
626    #[test]
627    fn pack_payload_stream_rejects_bad_checksum() {
628        let data = data();
629        let mut stream = zlib(&data);
630        let last = stream.len() - 1;
631        stream[last] ^= 0xff;
632        let err = read_all(whole(&stream, data.len() as u64)).expect_err("adler mismatch");
633        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
634    }
635}