Skip to main content

mkit_core/store/
source.rs

1//! Read-side object sources and adapters over the durable [`ObjectStore`].
2//!
3//! [`ObjectSource`] is the read trait shared by the store and its overlays;
4//! [`EphemeralSink`] is the in-memory snapshot overlay for query commands;
5//! [`DisplaySource`] is the display-only adapter that opts a render path out of
6//! per-read hash verification (#625). Extracted from `store.rs` (#633): these are
7//! consumers of the store's read API, distinct from its on-disk write/fsync
8//! machinery.
9
10use crate::hash::Hash;
11use crate::object::{Object, object_id_from_parts};
12use crate::serialize;
13
14use super::{MAX_RAW_OBJECT_SIZE, ObjectSink, ObjectStore, StoreError, StoreResult};
15
16/// Read source shared by [`ObjectStore`] and snapshot overlays, so
17/// tree-diff code can resolve objects from either the durable store or
18/// an in-memory [`EphemeralSink`].
19pub trait ObjectSource {
20    /// Read and integrity-verify the raw bytes of `h`.
21    ///
22    /// # Errors
23    /// [`StoreError::ObjectNotFound`] / [`StoreError::HashMismatch`] /
24    /// I/O errors, as for [`ObjectStore::read`].
25    fn read(&self, h: &Hash) -> StoreResult<Vec<u8>>;
26
27    /// Read and decode `h` into a typed [`Object`].
28    ///
29    /// # Errors
30    /// As [`Self::read`], plus decode errors.
31    fn read_object(&self, h: &Hash) -> StoreResult<Object> {
32        Ok(serialize::deserialize(&self.read(h)?)?)
33    }
34
35    /// Read `h` without integrity verification, for display-only
36    /// rendering — see [`ObjectStore::read_unverified`] for the full
37    /// policy (#625). Defaults to the fully-verifying [`Self::read`], so
38    /// every existing and third-party [`ObjectSource`] stays verifying
39    /// unless it explicitly opts in by overriding this method.
40    ///
41    /// # Errors
42    /// As [`Self::read`] (an opting-in override may narrow this to never
43    /// return [`StoreError::HashMismatch`]).
44    fn read_unverified(&self, h: &Hash) -> StoreResult<Vec<u8>> {
45        self.read(h)
46    }
47}
48
49impl ObjectSource for ObjectStore {
50    fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
51        ObjectStore::read(self, h)
52    }
53
54    fn read_unverified(&self, h: &Hash) -> StoreResult<Vec<u8>> {
55        ObjectStore::read_unverified(self, h)
56    }
57}
58
59/// In-memory object overlay for **ephemeral worktree snapshots**
60/// (`status`, `diff`, conflict/restore safety checks).
61///
62/// Writes never touch the durable store: objects whose hash already
63/// exists on disk are deduplicated against it (the store's
64/// visible-implies-durable invariant makes that safe), everything else
65/// lives in a private map that vanishes with the sink. This is what
66/// keeps query commands from (a) paying any durability cost, (b)
67/// growing `objects/` with throwaway snapshot trees, and (c) ever
68/// making a non-durable object *visible* where another writer's dedup
69/// could durably reference it.
70///
71/// Reads fall through to the underlying store, so diff walkers can
72/// resolve a snapshot tree that references committed objects.
73#[derive(Debug)]
74pub struct EphemeralSink<'s> {
75    store: &'s ObjectStore,
76    objects: std::sync::Mutex<std::collections::HashMap<Hash, Vec<u8>>>,
77}
78
79impl<'s> EphemeralSink<'s> {
80    /// Create an empty overlay over `store`.
81    #[must_use]
82    pub fn new(store: &'s ObjectStore) -> Self {
83        Self {
84            store,
85            objects: std::sync::Mutex::new(std::collections::HashMap::new()),
86        }
87    }
88}
89
90impl ObjectSink for EphemeralSink<'_> {
91    fn put(&self, bytes: &[u8]) -> StoreResult<Hash> {
92        self.put_parts(&[bytes])
93    }
94
95    fn put_parts(&self, parts: &[&[u8]]) -> StoreResult<Hash> {
96        let mut total: usize = 0;
97        for p in parts {
98            total = total
99                .checked_add(p.len())
100                .ok_or(StoreError::ObjectTooLarge)?;
101        }
102        if total > MAX_RAW_OBJECT_SIZE {
103            return Err(StoreError::ObjectTooLarge);
104        }
105        let h = object_id_from_parts(parts);
106        // Dedup against the durable store: visible store objects are durable
107        // by invariant, and skipping them keeps the overlay's memory bounded
108        // by the *changed* content, not the worktree. The overlay only ever
109        // materialises the bytes on a dedup miss.
110        if self.store.contains(&h) {
111            return Ok(h);
112        }
113        self.objects
114            .lock()
115            .expect("ephemeral sink mutex")
116            .entry(h)
117            .or_insert_with(|| {
118                let mut buf = Vec::with_capacity(total);
119                for p in parts {
120                    buf.extend_from_slice(p);
121                }
122                buf
123            });
124        Ok(h)
125    }
126
127    fn has(&self, h: &Hash) -> bool {
128        self.objects
129            .lock()
130            .expect("ephemeral sink mutex")
131            .contains_key(h)
132            || self.store.contains(h)
133    }
134}
135
136impl EphemeralSink<'_> {
137    /// The private-map half of a read: bytes this process already built
138    /// (via `put`/`put_parts`) and never persisted anywhere else to be
139    /// corrupted, shared by both [`ObjectSource::read`] and
140    /// [`ObjectSource::read_unverified`] below — the two differ only in
141    /// what they do on a miss.
142    fn overlay_get(&self, h: &Hash) -> Option<Vec<u8>> {
143        self.objects
144            .lock()
145            .expect("ephemeral sink mutex")
146            .get(h)
147            .cloned()
148    }
149}
150
151impl ObjectSource for EphemeralSink<'_> {
152    fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
153        match self.overlay_get(h) {
154            Some(bytes) => Ok(bytes),
155            None => self.store.read(h),
156        }
157    }
158
159    fn read_unverified(&self, h: &Hash) -> StoreResult<Vec<u8>> {
160        // Private-map hits are already trusted, same as the verifying
161        // `read` path above. Only the store fall-through actually skips a
162        // hash check.
163        match self.overlay_get(h) {
164            Some(bytes) => Ok(bytes),
165            None => self.store.read_unverified(h),
166        }
167    }
168}
169
170/// Adapter that makes any [`ObjectSource`] read without BLAKE3
171/// verification, for display-only rendering (`diff`, `show`, and the
172/// commit/merge/pull post-op summaries). Wrap the source once at the
173/// render call site — `DisplaySource::new(&store)` — and pass the
174/// wrapper wherever a generic `S: ObjectSource` render function expects
175/// its source. Every existing render function (`render_stat`,
176/// `emit_entry_patch`, `LoadedBlob::load`/`prefix`/`into_content`) works
177/// unchanged: the verify/no-verify policy lives entirely in which source
178/// gets passed in, never in a flag threaded through render code.
179///
180/// # When to use
181/// mkit never feeds unverified bytes into state that mkit itself writes,
182/// or into output that mkit's own tooling round-trips (format-patch →
183/// `git am`) — a diffstat or patch preview that only a human reads and
184/// discards is neither, so it's fair game. NEVER wrap a source feeding
185/// publication (commit/merge/rebase tree writes), dedup, fetch/apply, or
186/// any path whose output becomes durable state or gets applied elsewhere
187/// (e.g. the format-patch body in `git_tools.rs`, which is deliberately
188/// NOT wrapped because `git am` applies it into new commits). See
189/// [`ObjectStore::read_unverified`] for the full policy this wrapper
190/// exists to apply consistently (#625).
191pub struct DisplaySource<'a, S: ObjectSource + ?Sized> {
192    inner: &'a S,
193}
194
195impl<'a, S: ObjectSource + ?Sized> DisplaySource<'a, S> {
196    /// Wrap `inner` so reads through this adapter skip verification.
197    #[must_use]
198    pub fn new(inner: &'a S) -> Self {
199        Self { inner }
200    }
201}
202
203// Manual `Debug` (rather than `derive`) so this doesn't force an
204// unnecessary `S: Debug` bound on every generic render fn that only
205// needs `S: ObjectSource`.
206impl<S: ObjectSource + ?Sized> std::fmt::Debug for DisplaySource<'_, S> {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("DisplaySource").finish_non_exhaustive()
209    }
210}
211
212impl<S: ObjectSource + ?Sized> ObjectSource for DisplaySource<'_, S> {
213    fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
214        self.inner.read_unverified(h)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::layout::RepoLayout;
222    use std::fs::OpenOptions;
223    use std::io::{self, Seek, Write};
224    use tempfile::TempDir;
225
226    fn fresh_store() -> (TempDir, ObjectStore) {
227        let dir = TempDir::new().expect("tempdir");
228        let store = ObjectStore::init(&RepoLayout::single(dir.path())).expect("init");
229        (dir, store)
230    }
231
232    /// Flip the first byte of the on-disk object file for `h`, in place.
233    /// Shared by the `DisplaySource` corruption tests below — the same
234    /// "the bytes on disk no longer match `h`" setup the corruption tests
235    /// in `store.rs` use (deliberately duplicated there rather than
236    /// exposing a test-support seam).
237    fn corrupt_first_byte(store: &ObjectStore, h: &Hash, first_byte: u8) {
238        let path = store.path_for(h);
239        let mut f = OpenOptions::new()
240            .read(true)
241            .write(true)
242            .open(&path)
243            .unwrap();
244        f.seek(io::SeekFrom::Start(0)).unwrap();
245        f.write_all(&[first_byte ^ 0xFF]).unwrap();
246        f.sync_all().unwrap();
247    }
248
249    #[test]
250    fn display_source_delegates_to_unverified() {
251        // `DisplaySource` is the intended call-site adapter: wrapping the
252        // store must make `ObjectSource::read` succeed on an object that
253        // `ObjectStore::read` itself would reject.
254        let (_dir, store) = fresh_store();
255        let bytes = b"trustworthy".to_vec();
256        let h = store.write(&bytes).unwrap();
257        corrupt_first_byte(&store, &h, bytes[0]);
258        assert!(matches!(
259            store.read(&h).unwrap_err(),
260            StoreError::HashMismatch { .. }
261        ));
262
263        let display = DisplaySource::new(&store);
264        let mut corrupted = bytes.clone();
265        corrupted[0] ^= 0xFF;
266        assert_eq!(
267            display.read(&h).unwrap(),
268            corrupted,
269            "DisplaySource::read must delegate to the store's unverified read"
270        );
271    }
272
273    #[test]
274    fn ephemeral_sink_unverified_falls_through() {
275        // Two shapes an `EphemeralSink` reader can hit: a private-map
276        // object built by this snapshot (never touches disk, so nothing to
277        // corrupt) and a store-backed object corrupted on disk. Both must
278        // read fine through `DisplaySource<EphemeralSink>`.
279        let (_dir, store) = fresh_store();
280        let durable_bytes = b"trustworthy".to_vec();
281        let durable_h = store.write(&durable_bytes).unwrap();
282        corrupt_first_byte(&store, &durable_h, durable_bytes[0]);
283
284        let sink = EphemeralSink::new(&store);
285        let private_h = sink.put(b"snapshot-only").unwrap();
286
287        let display = DisplaySource::new(&sink);
288        assert_eq!(display.read(&private_h).unwrap(), b"snapshot-only");
289
290        let mut corrupted = durable_bytes.clone();
291        corrupted[0] ^= 0xFF;
292        assert_eq!(display.read(&durable_h).unwrap(), corrupted);
293    }
294
295    #[test]
296    fn read_unverified_default_is_verifying_read() {
297        // A minimal `ObjectSource` impl that only defines `read` must get
298        // fully-verifying behaviour from `read_unverified`'s default body
299        // — the whole safety net for third-party/future implementors that
300        // haven't opted in to the unverified path.
301        struct OnlyReadImpl<'s>(&'s ObjectStore);
302        impl ObjectSource for OnlyReadImpl<'_> {
303            fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
304                self.0.read(h)
305            }
306        }
307
308        let (_dir, store) = fresh_store();
309        let bytes = b"trustworthy".to_vec();
310        let h = store.write(&bytes).unwrap();
311        corrupt_first_byte(&store, &h, bytes[0]);
312
313        let only_read = OnlyReadImpl(&store);
314        assert!(matches!(
315            only_read.read_unverified(&h).unwrap_err(),
316            StoreError::HashMismatch { .. }
317        ));
318    }
319}