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 now = filetime::FileTime::now();
446    if filetime::set_file_times(pack_path, now, now).is_err() {
447        return false;
448    }
449    let touched = std::time::UNIX_EPOCH
450        + std::time::Duration::new(now.unix_seconds() as u64, now.nanoseconds());
451    pack::refresh_pack_bytes_signature(pack_path, touched);
452    true
453}
454
455/// Compressed bytes a [`PackPayloadStream`] reads from a mapped pack before it drops the
456/// mapping's pages from the resident set, so streaming a large object keeps a bounded
457/// window of the pack resident instead of the whole compressed entry.
458const STREAM_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
459
460/// Inflates a whole (non-delta) pack entry straight out of the mapped pack.
461///
462/// Yields exactly the declared payload size, then requires the zlib stream to end there, so
463/// truncated, oversized or checksum-corrupt entries surface as read errors.
464struct PackPayloadStream {
465    whole: pack::PackedWholeObject,
466    dec: Decompress,
467    pos: usize,
468    remaining: u64,
469    stream_ended: bool,
470    /// Compressed bytes consumed since the mapping's pages were last released.
471    resident: usize,
472}
473
474impl PackPayloadStream {
475    fn new(whole: pack::PackedWholeObject) -> Self {
476        Self {
477            pos: whole.zlib_offset,
478            remaining: whole.size,
479            whole,
480            dec: Decompress::new(true),
481            stream_ended: false,
482            resident: 0,
483        }
484    }
485
486    /// Inflate into `out`, returning the bytes produced.
487    fn inflate(&mut self, out: &mut [u8]) -> io::Result<usize> {
488        let input = &self.whole.pack[self.pos..self.whole.data_end];
489        let before_in = self.dec.total_in();
490        let before_out = self.dec.total_out();
491        let status = self
492            .dec
493            .decompress(input, out, FlushDecompress::None)
494            .map_err(|e| corrupt_pack_stream(&e.to_string()))?;
495        let consumed = (self.dec.total_in() - before_in) as usize;
496        let produced = (self.dec.total_out() - before_out) as usize;
497        self.pos += consumed;
498        self.resident += consumed;
499        if self.resident >= STREAM_RESIDENT_PACK_BYTES {
500            // The bytes stay readable; later reads fault pages back in from the page cache.
501            self.whole.pack.release_resident();
502            self.resident = 0;
503        }
504        if status == Status::StreamEnd {
505            self.stream_ended = true;
506        } else if consumed == 0 && produced == 0 {
507            return Err(corrupt_pack_stream("zlib stream truncated"));
508        }
509        Ok(produced)
510    }
511}
512
513fn corrupt_pack_stream(detail: &str) -> io::Error {
514    io::Error::new(
515        io::ErrorKind::InvalidData,
516        format!("corrupt packed object: {detail}"),
517    )
518}
519
520impl Read for PackPayloadStream {
521    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
522        if self.remaining == 0 {
523            let mut excess = [0u8; 1];
524            while !self.stream_ended {
525                if self.inflate(&mut excess)? > 0 {
526                    return Err(corrupt_pack_stream("more data than declared size"));
527                }
528            }
529            return Ok(0);
530        }
531        if buf.is_empty() {
532            return Ok(0);
533        }
534        let max = usize::try_from(self.remaining).map_or(buf.len(), |r| r.min(buf.len()));
535        loop {
536            if self.stream_ended {
537                return Err(corrupt_pack_stream("less data than declared size"));
538            }
539            let produced = self.inflate(&mut buf[..max])?;
540            if produced > 0 {
541                self.remaining -= produced as u64;
542                return Ok(produced);
543            }
544        }
545    }
546}
547
548fn stream_from_buffered_read(
549    store: &PackedObjects,
550    oid: &ObjectId,
551) -> Result<Option<ObjectStream<'static>>> {
552    let Some(obj) = store.read(oid)? else {
553        return Ok(None);
554    };
555    let size = u64::try_from(obj.data.len())
556        .map_err(|_| Error::CorruptObject(format!("object size overflow for {}", oid.to_hex())))?;
557    Ok(Some(ObjectStream {
558        kind: obj.kind,
559        size,
560        reader: Box::new(Cursor::new(obj.data)),
561    }))
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use flate2::write::ZlibEncoder;
568    use flate2::Compression;
569    use std::io::Write;
570
571    use crate::pack_map::PackData;
572
573    const TRAILER: usize = 20;
574
575    fn zlib(data: &[u8]) -> Vec<u8> {
576        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
577        enc.write_all(data).expect("deflate");
578        enc.finish().expect("deflate")
579    }
580
581    /// A fake pack holding one whole entry: some leading bytes, the zlib stream, a trailer.
582    fn whole(zlib_stream: &[u8], declared_size: u64) -> pack::PackedWholeObject {
583        let mut bytes = b"PACK\0\0\0\x02\0\0\0\x01\xb0".to_vec();
584        let zlib_offset = bytes.len();
585        bytes.extend_from_slice(zlib_stream);
586        let data_end = bytes.len();
587        bytes.extend_from_slice(&[0u8; TRAILER]);
588        pack::PackedWholeObject {
589            pack: PackData::from_owned(bytes),
590            zlib_offset,
591            data_end,
592            kind: ObjectKind::Blob,
593            size: declared_size,
594        }
595    }
596
597    fn read_all(whole: pack::PackedWholeObject) -> io::Result<Vec<u8>> {
598        let mut stream = PackPayloadStream::new(whole);
599        let mut out = Vec::new();
600        let mut chunk = [0u8; 1000];
601        loop {
602            match stream.read(&mut chunk)? {
603                0 => return Ok(out),
604                n => out.extend_from_slice(&chunk[..n]),
605            }
606        }
607    }
608
609    fn data() -> Vec<u8> {
610        (0..200_000u32)
611            .flat_map(|i| (i % 251).to_le_bytes())
612            .collect()
613    }
614
615    #[test]
616    fn pack_payload_stream_yields_the_whole_payload() {
617        let data = data();
618        let out = read_all(whole(&zlib(&data), data.len() as u64)).expect("stream");
619        assert!(out == data);
620    }
621
622    #[test]
623    fn pack_payload_stream_rejects_truncated_entry() {
624        let data = data();
625        let stream = zlib(&data);
626        let err =
627            read_all(whole(&stream[..stream.len() / 2], data.len() as u64)).expect_err("truncated");
628        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
629    }
630
631    #[test]
632    fn pack_payload_stream_rejects_size_mismatch_both_ways() {
633        let data = data();
634        let stream = zlib(&data);
635        let err = read_all(whole(&stream, data.len() as u64 + 1)).expect_err("short");
636        assert!(err.to_string().contains("less data"), "{err}");
637        let err = read_all(whole(&stream, data.len() as u64 - 1)).expect_err("long");
638        assert!(err.to_string().contains("more data"), "{err}");
639    }
640
641    #[test]
642    fn pack_payload_stream_rejects_bad_checksum() {
643        let data = data();
644        let mut stream = zlib(&data);
645        let last = stream.len() - 1;
646        stream[last] ^= 0xff;
647        let err = read_all(whole(&stream, data.len() as u64)).expect_err("adler mismatch");
648        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
649    }
650}