Skip to main content

grit_lib/odb/store/
midx.rs

1//! Multi-pack-index object backend ([`MidxObjects`]).
2//!
3//! OID membership and prefix search use the prepared MIDX chain; object bytes are read at the
4//! layer-resolved pack offset via [`PreparedMidxChain::try_read_object`].
5
6use std::collections::HashSet;
7use std::io::Cursor;
8use std::ops::ControlFlow;
9use std::sync::{Arc, RwLock};
10
11use crate::diagnostics::NullDiagnostics;
12use crate::error::{Error, Result};
13use crate::midx::{prepared_midx_chain_strict, PreparedMidxChain};
14use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo};
15use crate::pack_store::PackStore;
16
17use super::{ObjectStore, ObjectStream, PackedObjects};
18
19/// Whether [`MidxObjects`] can serve reads for this repository.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum MidxObjectsStatus {
22    /// `core.multiPackIndex` was off at construction; this backend is inert.
23    Disabled,
24    /// A usable MIDX chain is loaded.
25    Active,
26    /// MIDX was requested but no layer could be loaded (missing or corrupt); fall back to packs.
27    Unusable,
28}
29
30/// MIDX-backed read-only [`ObjectStore`] for one `objects/` directory.
31///
32/// Preparation and payload reads use the supplied [`PackStore`], including calls
33/// made outside an explicit pack-read context. Strict and permissive MIDX views
34/// in that store share validated layer mappings. Checksums are checked once per
35/// layer with bounded residency; lookup pages are faulted in as needed.
36/// Independently opened stores keep independent snapshots. Call
37/// [`ObjectStore::refresh`] after replacing MIDX or pack files externally.
38pub struct MidxObjects {
39    packs: PackedObjects,
40    multi_pack_index: bool,
41    status: RwLock<MidxObjectsStatus>,
42}
43
44impl std::fmt::Debug for MidxObjects {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("MidxObjects")
47            .field("objects_dir", &self.packs.pack_store().objects_dir())
48            .field("multi_pack_index", &self.multi_pack_index)
49            .field("status", &self.status())
50            .finish()
51    }
52}
53
54impl MidxObjects {
55    /// Open MIDX reads for `store`'s `objects/` directory.
56    ///
57    /// `multi_pack_index` mirrors `core.multiPackIndex` at construction time; the store does not
58    /// read config itself.
59    ///
60    /// # Errors
61    ///
62    /// Propagates MIDX chain preparation failures other than a missing or skipped index.
63    pub fn new(store: Arc<PackStore>, multi_pack_index: bool) -> Result<Self> {
64        let packs = PackedObjects::new(Arc::clone(&store));
65        let status = if multi_pack_index {
66            PackStore::with_context(Arc::clone(&store), || {
67                classify_midx_status(store.objects_dir())
68            })?
69        } else {
70            MidxObjectsStatus::Disabled
71        };
72        Ok(Self {
73            packs,
74            multi_pack_index,
75            status: RwLock::new(status),
76        })
77    }
78
79    /// Whether MIDX-backed reads are active for this store.
80    #[must_use]
81    pub fn status(&self) -> MidxObjectsStatus {
82        self.status
83            .read()
84            .map(|g| *g)
85            .unwrap_or(MidxObjectsStatus::Unusable)
86    }
87
88    /// Whether `core.multiPackIndex` was enabled when this store was built.
89    #[must_use]
90    pub fn multi_pack_index_enabled(&self) -> bool {
91        self.multi_pack_index
92    }
93
94    /// Pack index basenames covered by the active MIDX chain (empty when inactive).
95    ///
96    /// Callers skip these indexes in [`PackedObjects`] so MIDX-covered OIDs are not scanned twice.
97    ///
98    /// # Errors
99    ///
100    /// Propagates MIDX chain load failures.
101    pub fn covered_pack_basenames(&self) -> Result<HashSet<String>> {
102        let Some(chain) = self.active_chain()? else {
103            return Ok(HashSet::new());
104        };
105        Ok(chain.covered_pack_basenames())
106    }
107
108    /// Underlying pack backend used for object payload reads.
109    #[must_use]
110    pub fn packed_objects(&self) -> &PackedObjects {
111        &self.packs
112    }
113
114    fn active_chain(&self) -> Result<Option<Arc<PreparedMidxChain>>> {
115        if self.status() != MidxObjectsStatus::Active {
116            return Ok(None);
117        }
118        let store = self.packs.pack_store();
119        match PackStore::with_context(Arc::clone(store), || {
120            prepared_midx_chain_strict(store.objects_dir())
121        }) {
122            Ok(chain) => Ok(chain),
123            Err(Error::Midx(_)) => Ok(None),
124            Err(err) => Err(err),
125        }
126    }
127
128    fn read_via_chain(&self, oid: &ObjectId) -> Result<Option<Object>> {
129        PackStore::with_context(Arc::clone(self.packs.pack_store()), || {
130            let Some(chain) = self.active_chain()? else {
131                return Ok(None);
132            };
133            chain.try_read_object(oid, &NullDiagnostics)
134        })
135    }
136
137    fn read_info_via_chain(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
138        PackStore::with_context(Arc::clone(self.packs.pack_store()), || {
139            let Some(chain) = self.active_chain()? else {
140                return Ok(None);
141            };
142            chain.try_read_info(oid, &NullDiagnostics)
143        })
144    }
145}
146
147impl ObjectStore for MidxObjects {
148    fn hash_algo(&self) -> HashAlgo {
149        self.packs.hash_algo()
150    }
151
152    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
153        self.read_via_chain(oid)
154    }
155
156    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
157        self.read_info_via_chain(oid)
158    }
159
160    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
161        let Some(info) = self.read_info(oid)? else {
162            return Ok(None);
163        };
164        if let Some(stream) = self.packs.open_stream(oid)? {
165            return Ok(Some(stream));
166        }
167        let Some(obj) = self.read(oid)? else {
168            return Ok(None);
169        };
170        let size = u64::try_from(obj.data.len()).map_err(|_| {
171            Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
172        })?;
173        Ok(Some(ObjectStream {
174            kind: info.kind,
175            size,
176            reader: Box::new(Cursor::new(obj.data)),
177        }))
178    }
179
180    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
181        let Some(chain) = self.active_chain()? else {
182            return Ok(());
183        };
184        chain.for_each_listed_object(f)
185    }
186
187    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
188        if prefix.len() > self.hash_algo().hex_len() {
189            return Ok(());
190        }
191        let Some(chain) = self.active_chain()? else {
192            return Ok(());
193        };
194        let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
195        chain.lookup_prefix(&prefix, limit, out)
196    }
197
198    fn refresh(&self) -> Result<bool> {
199        if !self.multi_pack_index {
200            return Ok(false);
201        }
202        let store = self.packs.pack_store();
203        store.invalidate_all();
204        let new_status = PackStore::with_context(Arc::clone(store), || {
205            classify_midx_status(store.objects_dir())
206        })?;
207        let mut guard = self
208            .status
209            .write()
210            .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?;
211        let changed = *guard != new_status;
212        *guard = new_status;
213        Ok(changed)
214    }
215}
216
217fn classify_midx_status(objects_dir: &std::path::Path) -> Result<MidxObjectsStatus> {
218    match prepared_midx_chain_strict(objects_dir) {
219        Ok(Some(_)) => Ok(MidxObjectsStatus::Active),
220        Ok(None) | Err(Error::Midx(_)) | Err(Error::CorruptObject(_)) => {
221            Ok(MidxObjectsStatus::Unusable)
222        }
223        Err(err) => Err(err),
224    }
225}