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`
143    /// or `.promisor` file.
144    ///
145    /// Used by repack when retaining unreachable storage from packs that will be removed.
146    ///
147    /// # Errors
148    ///
149    /// Propagates pack index enumeration failures.
150    pub fn for_each_removable_pack_object(
151        &self,
152        f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
153    ) -> Result<()> {
154        self.with_pack(|objects_dir| for_each_removable_pack_object_ids(objects_dir, f))
155    }
156
157    /// Touch the mtime of the pack file holding `oid` (Git pack freshen), when not cruft.
158    ///
159    /// # Errors
160    ///
161    /// Propagates I/O failures from pack listing or utime.
162    pub fn freshen_pack_containing(&self, oid: &ObjectId) -> Result<bool> {
163        self.with_pack(|objects_dir| {
164            let indexes = read_local_pack_indexes_cached(objects_dir)?;
165            for idx in &indexes {
166                if !idx.contains(oid)? {
167                    continue;
168                }
169                if idx.is_cruft {
170                    return Ok(false);
171                }
172                return Ok(touch_pack_mtime(&idx.pack_path));
173            }
174            Ok(false)
175        })
176    }
177
178    fn with_pack<R>(&self, f: impl FnOnce(&std::path::Path) -> Result<R>) -> Result<R> {
179        if PackStore::read_context_matches_objects_dir(self.store.objects_dir()) {
180            return f(self.store.objects_dir());
181        }
182        PackStore::with_context(Arc::clone(&self.store), || f(self.store.objects_dir()))
183    }
184
185    fn hash_algo_from_indexes(&self) -> HashAlgo {
186        self.with_pack(|objects_dir| {
187            let Ok(indexes) = read_local_pack_indexes_cached(objects_dir) else {
188                return Ok(HashAlgo::Sha1);
189            };
190            Ok(indexes
191                .first()
192                .and_then(|idx| HashAlgo::from_len(idx.hash_bytes()))
193                .unwrap_or(HashAlgo::Sha1))
194        })
195        .unwrap_or(HashAlgo::Sha1)
196    }
197
198    fn read_from_packs(&self, oid: &ObjectId) -> Result<Option<Object>> {
199        self.with_pack(
200            |objects_dir| match pack::read_object_from_packs(objects_dir, oid) {
201                Ok(obj) => Ok(Some(obj)),
202                Err(Error::ObjectNotFound(_)) => Ok(None),
203                Err(err) => Err(err),
204            },
205        )
206    }
207
208    fn read_from_packs_into(
209        &self,
210        oid: &ObjectId,
211        buf: &mut Vec<u8>,
212    ) -> Result<Option<ObjectKind>> {
213        self.with_pack(|objects_dir| {
214            match pack::read_object_from_packs_into(objects_dir, oid, buf) {
215                Ok(kind) => Ok(Some(kind)),
216                Err(Error::ObjectNotFound(_)) => Ok(None),
217                Err(err) => Err(err),
218            }
219        })
220    }
221
222    fn read_info_from_packs(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
223        self.with_pack(
224            |objects_dir| match pack::read_object_info_from_packs(objects_dir, oid) {
225                Ok(info) => Ok(Some(info)),
226                Err(Error::ObjectNotFound(_)) => Ok(None),
227                Err(err) => Err(err),
228            },
229        )
230    }
231}
232
233impl ObjectStore for PackedObjects {
234    fn hash_algo(&self) -> HashAlgo {
235        self.hash_algo_from_indexes()
236    }
237
238    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
239        self.read_from_packs(oid)
240    }
241
242    fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
243        self.read_from_packs_into(oid, buf)
244    }
245
246    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
247        self.read_info_from_packs(oid)
248    }
249
250    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
251        self.with_pack(|dir| {
252            let mut entry = pack::locate_packed_whole_object(dir, oid, true)?;
253            if matches!(entry, pack::PackedEntry::Absent) && reprepare_pack_directory_on_miss(dir)?
254            {
255                entry = pack::locate_packed_whole_object(dir, oid, true)?;
256            }
257            let pack::PackedEntry::Whole(whole) = entry else {
258                return stream_from_buffered_read(self, oid);
259            };
260            Ok(Some(ObjectStream {
261                kind: whole.kind,
262                size: whole.size,
263                reader: Box::new(PackPayloadStream::new(whole)),
264            }))
265        })
266    }
267
268    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
269        self.with_pack(|objects_dir| {
270            let indexes = read_local_pack_indexes_cached(objects_dir)?;
271            let mut seen = HashSet::new();
272            for idx in &indexes {
273                for entry in idx.iter() {
274                    let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
275                        continue;
276                    };
277                    if !seen.insert(oid) {
278                        continue;
279                    }
280                    if f(&oid).is_break() {
281                        return Ok(());
282                    }
283                }
284            }
285            Ok(())
286        })
287    }
288
289    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
290        if prefix.len() > self.hash_algo().hex_len() {
291            return Ok(());
292        }
293        let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
294        self.with_pack(|objects_dir| {
295            let indexes = read_local_pack_indexes_cached(objects_dir)?;
296            let mut seen = HashSet::new();
297            let mut count = 0usize;
298            for idx in &indexes {
299                collect_prefix_from_index(idx, &prefix, limit, &mut count, out, &mut seen)?;
300                if limit != 0 && count >= limit {
301                    break;
302                }
303            }
304            Ok(())
305        })
306    }
307
308    fn refresh(&self) -> Result<bool> {
309        self.with_pack(reprepare_pack_directory_on_miss)
310    }
311}
312
313fn for_each_removable_pack_object_ids(
314    objects_dir: &Path,
315    f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
316) -> Result<()> {
317    let pack_dir = objects_dir.join("pack");
318    let indexes = read_local_pack_indexes_cached(objects_dir)?;
319    let mut seen = HashSet::new();
320    for idx in indexes {
321        if idx.is_promisor || pack_index_has_keep_marker(&pack_dir, &idx.pack_path) {
322            continue;
323        }
324        let hash_bytes = idx.hash_bytes();
325        for entry in idx.iter() {
326            if entry.oid().len() != hash_bytes {
327                continue;
328            }
329            let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
330                continue;
331            };
332            if !seen.insert(oid) {
333                continue;
334            }
335            if f(&oid).is_break() {
336                return Ok(());
337            }
338        }
339    }
340    Ok(())
341}
342
343fn pack_index_has_keep_marker(pack_dir: &Path, pack_path: &Path) -> bool {
344    let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
345        return false;
346    };
347    pack_dir.join(format!("{stem}.keep")).is_file()
348}
349
350fn collect_kept_pack_object_ids(objects_dir: &Path) -> Result<HashSet<ObjectId>> {
351    let pack_dir = objects_dir.join("pack");
352    let mut kept = HashSet::new();
353    if !pack_dir.is_dir() {
354        return Ok(kept);
355    }
356    for entry in std::fs::read_dir(&pack_dir).map_err(Error::Io)? {
357        let entry = entry.map_err(Error::Io)?;
358        let path = entry.path();
359        if path.extension().is_some_and(|ext| ext == "keep") {
360            let idx_path = path.with_extension("idx");
361            if idx_path.is_file() {
362                if let Ok(oids) = pack::read_idx_object_ids(&idx_path) {
363                    kept.extend(oids);
364                }
365            }
366        }
367    }
368    Ok(kept)
369}
370
371fn filtered_indexes(
372    objects_dir: &std::path::Path,
373    filter: PackFilter,
374) -> Result<Vec<Arc<PackIndex>>> {
375    let indexes = read_local_pack_indexes_cached(objects_dir)?;
376    Ok(indexes
377        .into_iter()
378        .filter(|idx| {
379            (filter.include_promisor || !idx.is_promisor) && (filter.include_cruft || !idx.is_cruft)
380        })
381        .collect())
382}
383
384fn collect_prefix_from_index(
385    idx: &PackIndex,
386    prefix: &ObjectIdPrefix,
387    limit: usize,
388    count: &mut usize,
389    out: &mut Vec<ObjectId>,
390    seen: &mut HashSet<ObjectId>,
391) -> Result<()> {
392    let lo = lower_bound_oid(idx, prefix.lowest().as_bytes());
393    let hi = upper_bound_oid(idx, prefix.highest().as_bytes());
394    for i in lo..hi {
395        let digest = idx.oid_at(i);
396        if !prefix.matches_digest(digest) {
397            continue;
398        }
399        let Ok(oid) = ObjectId::from_bytes(digest) else {
400            continue;
401        };
402        if !seen.insert(oid) {
403            continue;
404        }
405        out.push(oid);
406        *count += 1;
407        if limit != 0 && *count >= limit {
408            break;
409        }
410    }
411    Ok(())
412}
413
414fn lower_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
415    let mut lo = 0usize;
416    let mut hi = idx.len();
417    while lo < hi {
418        let mid = lo + (hi - lo) / 2;
419        if idx.oid_at(mid) < needle {
420            lo = mid + 1;
421        } else {
422            hi = mid;
423        }
424    }
425    lo
426}
427
428fn upper_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
429    let mut lo = 0usize;
430    let mut hi = idx.len();
431    while lo < hi {
432        let mid = lo + (hi - lo) / 2;
433        if idx.oid_at(mid) <= needle {
434            lo = mid + 1;
435        } else {
436            hi = mid;
437        }
438    }
439    lo
440}
441
442/// Freshen `pack_path` (like rewriting objects it already holds) so age-based pruning
443/// treats its objects as new. Returns `false` when the times cannot be set.
444pub(crate) fn touch_pack_mtime(pack_path: &std::path::Path) -> bool {
445    let Some(touched) = crate::fs_readonly::set_file_times_now(pack_path) else {
446        return false;
447    };
448    pack::refresh_pack_bytes_signature(pack_path, touched);
449    true
450}
451
452/// Compressed bytes a [`PackPayloadStream`] reads from a mapped pack before it drops the
453/// mapping's pages from the resident set, so streaming a large object keeps a bounded
454/// window of the pack resident instead of the whole compressed entry.
455const STREAM_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
456
457/// Inflates a whole (non-delta) pack entry straight out of the mapped pack.
458///
459/// Yields exactly the declared payload size, then requires the zlib stream to end there, so
460/// truncated, oversized or checksum-corrupt entries surface as read errors.
461struct PackPayloadStream {
462    whole: pack::PackedWholeObject,
463    dec: Decompress,
464    pos: usize,
465    remaining: u64,
466    stream_ended: bool,
467    /// Compressed bytes consumed since the mapping's pages were last released.
468    resident: usize,
469}
470
471impl PackPayloadStream {
472    fn new(whole: pack::PackedWholeObject) -> Self {
473        Self {
474            pos: whole.zlib_offset,
475            remaining: whole.size,
476            whole,
477            dec: Decompress::new(true),
478            stream_ended: false,
479            resident: 0,
480        }
481    }
482
483    /// Inflate into `out`, returning the bytes produced.
484    fn inflate(&mut self, out: &mut [u8]) -> io::Result<usize> {
485        let input = &self.whole.pack[self.pos..self.whole.data_end];
486        let before_in = self.dec.total_in();
487        let before_out = self.dec.total_out();
488        let status = self
489            .dec
490            .decompress(input, out, FlushDecompress::None)
491            .map_err(|e| corrupt_pack_stream(&e.to_string()))?;
492        let consumed = (self.dec.total_in() - before_in) as usize;
493        let produced = (self.dec.total_out() - before_out) as usize;
494        self.pos += consumed;
495        self.resident += consumed;
496        if self.resident >= STREAM_RESIDENT_PACK_BYTES {
497            // The bytes stay readable; later reads fault pages back in from the page cache.
498            self.whole.pack.release_resident();
499            self.resident = 0;
500        }
501        if status == Status::StreamEnd {
502            self.stream_ended = true;
503        } else if consumed == 0 && produced == 0 {
504            return Err(corrupt_pack_stream("zlib stream truncated"));
505        }
506        Ok(produced)
507    }
508}
509
510fn corrupt_pack_stream(detail: &str) -> io::Error {
511    io::Error::new(
512        io::ErrorKind::InvalidData,
513        format!("corrupt packed object: {detail}"),
514    )
515}
516
517impl Read for PackPayloadStream {
518    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
519        if self.remaining == 0 {
520            let mut excess = [0u8; 1];
521            while !self.stream_ended {
522                if self.inflate(&mut excess)? > 0 {
523                    return Err(corrupt_pack_stream("more data than declared size"));
524                }
525            }
526            return Ok(0);
527        }
528        if buf.is_empty() {
529            return Ok(0);
530        }
531        let max = usize::try_from(self.remaining).map_or(buf.len(), |r| r.min(buf.len()));
532        loop {
533            if self.stream_ended {
534                return Err(corrupt_pack_stream("less data than declared size"));
535            }
536            let produced = self.inflate(&mut buf[..max])?;
537            if produced > 0 {
538                self.remaining -= produced as u64;
539                return Ok(produced);
540            }
541        }
542    }
543}
544
545fn stream_from_buffered_read(
546    store: &PackedObjects,
547    oid: &ObjectId,
548) -> Result<Option<ObjectStream<'static>>> {
549    let Some(obj) = store.read(oid)? else {
550        return Ok(None);
551    };
552    let size = u64::try_from(obj.data.len())
553        .map_err(|_| Error::CorruptObject(format!("object size overflow for {}", oid.to_hex())))?;
554    Ok(Some(ObjectStream {
555        kind: obj.kind,
556        size,
557        reader: Box::new(Cursor::new(obj.data)),
558    }))
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use flate2::write::ZlibEncoder;
565    use flate2::Compression;
566    use std::io::Write;
567
568    use crate::pack_map::PackData;
569
570    const TRAILER: usize = 20;
571
572    fn zlib(data: &[u8]) -> Vec<u8> {
573        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
574        enc.write_all(data).expect("deflate");
575        enc.finish().expect("deflate")
576    }
577
578    /// A fake pack holding one whole entry: some leading bytes, the zlib stream, a trailer.
579    fn whole(zlib_stream: &[u8], declared_size: u64) -> pack::PackedWholeObject {
580        let mut bytes = b"PACK\0\0\0\x02\0\0\0\x01\xb0".to_vec();
581        let zlib_offset = bytes.len();
582        bytes.extend_from_slice(zlib_stream);
583        let data_end = bytes.len();
584        bytes.extend_from_slice(&[0u8; TRAILER]);
585        pack::PackedWholeObject {
586            pack: PackData::from_owned(bytes),
587            zlib_offset,
588            data_end,
589            kind: ObjectKind::Blob,
590            size: declared_size,
591        }
592    }
593
594    fn read_all(whole: pack::PackedWholeObject) -> io::Result<Vec<u8>> {
595        let mut stream = PackPayloadStream::new(whole);
596        let mut out = Vec::new();
597        let mut chunk = [0u8; 1000];
598        loop {
599            match stream.read(&mut chunk)? {
600                0 => return Ok(out),
601                n => out.extend_from_slice(&chunk[..n]),
602            }
603        }
604    }
605
606    fn data() -> Vec<u8> {
607        (0..200_000u32)
608            .flat_map(|i| (i % 251).to_le_bytes())
609            .collect()
610    }
611
612    #[test]
613    fn pack_payload_stream_yields_the_whole_payload() {
614        let data = data();
615        let out = read_all(whole(&zlib(&data), data.len() as u64)).expect("stream");
616        assert!(out == data);
617    }
618
619    #[test]
620    fn pack_payload_stream_rejects_truncated_entry() {
621        let data = data();
622        let stream = zlib(&data);
623        let err =
624            read_all(whole(&stream[..stream.len() / 2], data.len() as u64)).expect_err("truncated");
625        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
626    }
627
628    #[test]
629    fn pack_payload_stream_rejects_size_mismatch_both_ways() {
630        let data = data();
631        let stream = zlib(&data);
632        let err = read_all(whole(&stream, data.len() as u64 + 1)).expect_err("short");
633        assert!(err.to_string().contains("less data"), "{err}");
634        let err = read_all(whole(&stream, data.len() as u64 - 1)).expect_err("long");
635        assert!(err.to_string().contains("more data"), "{err}");
636    }
637
638    #[test]
639    fn pack_payload_stream_rejects_bad_checksum() {
640        let data = data();
641        let mut stream = zlib(&data);
642        let last = stream.len() - 1;
643        stream[last] ^= 0xff;
644        let err = read_all(whole(&stream, data.len() as u64)).expect_err("adler mismatch");
645        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
646    }
647}