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    /// Overrides the trait default (`deserialize(self.read(h)?)`, which
59    /// would decode a `Tree`/`ChunkedBlob` twice — once inside `read`'s
60    /// id verification, once again here) to delegate to the inherent
61    /// [`ObjectStore::read_object`], which decodes once and reuses the
62    /// result. Every caller generic over `S: ObjectSource` (`diff::load_tree`,
63    /// `LoadedBlob::load`, …) resolves `read_object` through this trait
64    /// impl rather than the inherent method even when `S = ObjectStore`,
65    /// so without this override those call sites — the primary
66    /// Tree/ChunkedBlob decoding paths — would keep paying for the
67    /// double decode the inherent method was written to avoid.
68    fn read_object(&self, h: &Hash) -> StoreResult<Object> {
69        ObjectStore::read_object(self, h)
70    }
71}
72
73/// In-memory object overlay for **ephemeral worktree snapshots**
74/// (`status`, `diff`, conflict/restore safety checks).
75///
76/// Writes never touch the durable store: objects whose hash already
77/// exists on disk are deduplicated against it (the store's
78/// visible-implies-durable invariant makes that safe), everything else
79/// lives in a private map that vanishes with the sink. This is what
80/// keeps query commands from (a) paying any durability cost, (b)
81/// growing `objects/` with throwaway snapshot trees, and (c) ever
82/// making a non-durable object *visible* where another writer's dedup
83/// could durably reference it.
84///
85/// Reads fall through to the underlying store, so diff walkers can
86/// resolve a snapshot tree that references committed objects.
87#[derive(Debug)]
88pub struct EphemeralSink<'s> {
89    store: &'s ObjectStore,
90    objects: std::sync::Mutex<std::collections::HashMap<Hash, Vec<u8>>>,
91}
92
93impl<'s> EphemeralSink<'s> {
94    /// Create an empty overlay over `store`.
95    #[must_use]
96    pub fn new(store: &'s ObjectStore) -> Self {
97        Self {
98            store,
99            objects: std::sync::Mutex::new(std::collections::HashMap::new()),
100        }
101    }
102}
103
104impl ObjectSink for EphemeralSink<'_> {
105    fn put(&self, bytes: &[u8]) -> StoreResult<Hash> {
106        self.put_parts(&[bytes])
107    }
108
109    fn put_parts(&self, parts: &[&[u8]]) -> StoreResult<Hash> {
110        let mut total: usize = 0;
111        for p in parts {
112            total = total
113                .checked_add(p.len())
114                .ok_or(StoreError::ObjectTooLarge)?;
115        }
116        if total > MAX_RAW_OBJECT_SIZE {
117            return Err(StoreError::ObjectTooLarge);
118        }
119        let h = object_id_from_parts(parts);
120        // Dedup against the durable store: visible store objects are durable
121        // by invariant, and skipping them keeps the overlay's memory bounded
122        // by the *changed* content, not the worktree. The overlay only ever
123        // materialises the bytes on a dedup miss.
124        if self.store.contains(&h) {
125            return Ok(h);
126        }
127        self.objects
128            .lock()
129            .expect("ephemeral sink mutex")
130            .entry(h)
131            .or_insert_with(|| {
132                let mut buf = Vec::with_capacity(total);
133                for p in parts {
134                    buf.extend_from_slice(p);
135                }
136                buf
137            });
138        Ok(h)
139    }
140
141    fn has(&self, h: &Hash) -> bool {
142        self.objects
143            .lock()
144            .expect("ephemeral sink mutex")
145            .contains_key(h)
146            || self.store.contains(h)
147    }
148}
149
150impl EphemeralSink<'_> {
151    /// The private-map half of a read: bytes this process already built
152    /// (via `put`/`put_parts`) and never persisted anywhere else to be
153    /// corrupted, shared by both [`ObjectSource::read`] and
154    /// [`ObjectSource::read_unverified`] below — the two differ only in
155    /// what they do on a miss.
156    fn overlay_get(&self, h: &Hash) -> Option<Vec<u8>> {
157        self.objects
158            .lock()
159            .expect("ephemeral sink mutex")
160            .get(h)
161            .cloned()
162    }
163}
164
165impl ObjectSource for EphemeralSink<'_> {
166    fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
167        match self.overlay_get(h) {
168            Some(bytes) => Ok(bytes),
169            None => self.store.read(h),
170        }
171    }
172
173    /// Overrides the trait default for the same reason `impl ObjectSource
174    /// for ObjectStore` does: a private-map hit only ever needs one
175    /// decode (the default's `deserialize(self.read(h)?)` already gives
176    /// that), but a store fall-through would otherwise re-decode a
177    /// `Tree`/`ChunkedBlob` a second time on top of the decode
178    /// `ObjectStore::read`'s id verification already does internally.
179    /// Delegating to `self.store.read_object` on a miss reuses that
180    /// store-side decode instead — `diff`/`status`, which resolve staged
181    /// trees through this overlay, are the paths this matters for.
182    fn read_object(&self, h: &Hash) -> StoreResult<Object> {
183        match self.overlay_get(h) {
184            Some(bytes) => Ok(serialize::deserialize(&bytes)?),
185            None => self.store.read_object(h),
186        }
187    }
188
189    fn read_unverified(&self, h: &Hash) -> StoreResult<Vec<u8>> {
190        // Private-map hits are already trusted, same as the verifying
191        // `read` path above. Only the store fall-through actually skips a
192        // hash check.
193        match self.overlay_get(h) {
194            Some(bytes) => Ok(bytes),
195            None => self.store.read_unverified(h),
196        }
197    }
198}
199
200/// Adapter that makes any [`ObjectSource`] read without BLAKE3
201/// verification, for display-only rendering (`diff`, `show`, and the
202/// commit/merge/pull post-op summaries). Wrap the source once at the
203/// render call site — `DisplaySource::new(&store)` — and pass the
204/// wrapper wherever a generic `S: ObjectSource` render function expects
205/// its source. Every existing render function (`render_stat`,
206/// `emit_entry_patch`, `LoadedBlob::load`/`prefix`/`into_content`) works
207/// unchanged: the verify/no-verify policy lives entirely in which source
208/// gets passed in, never in a flag threaded through render code.
209///
210/// # When to use
211/// mkit never feeds unverified bytes into state that mkit itself writes,
212/// or into output that mkit's own tooling round-trips (format-patch →
213/// `git am`) — a diffstat or patch preview that only a human reads and
214/// discards is neither, so it's fair game. NEVER wrap a source feeding
215/// publication (commit/merge/rebase tree writes), dedup, fetch/apply, or
216/// any path whose output becomes durable state or gets applied elsewhere
217/// (e.g. the format-patch body in `git_tools.rs`, which is deliberately
218/// NOT wrapped because `git am` applies it into new commits), including
219/// [`crate::verify::build_disclosure_from`], whose bundles are published
220/// proofs. See
221/// [`ObjectStore::read_unverified`] for the full policy this wrapper
222/// exists to apply consistently (#625).
223pub struct DisplaySource<'a, S: ObjectSource + ?Sized> {
224    inner: &'a S,
225}
226
227impl<'a, S: ObjectSource + ?Sized> DisplaySource<'a, S> {
228    /// Wrap `inner` so reads through this adapter skip verification.
229    #[must_use]
230    pub fn new(inner: &'a S) -> Self {
231        Self { inner }
232    }
233}
234
235// Manual `Debug` (rather than `derive`) so this doesn't force an
236// unnecessary `S: Debug` bound on every generic render fn that only
237// needs `S: ObjectSource`.
238impl<S: ObjectSource + ?Sized> std::fmt::Debug for DisplaySource<'_, S> {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        f.debug_struct("DisplaySource").finish_non_exhaustive()
241    }
242}
243
244impl<S: ObjectSource + ?Sized> ObjectSource for DisplaySource<'_, S> {
245    fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
246        self.inner.read_unverified(h)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::layout::RepoLayout;
254    use std::fs::OpenOptions;
255    use std::io::{self, Seek, Write};
256    use tempfile::TempDir;
257
258    fn fresh_store() -> (TempDir, ObjectStore) {
259        let dir = TempDir::new().expect("tempdir");
260        let store = ObjectStore::init(&RepoLayout::single(dir.path())).expect("init");
261        (dir, store)
262    }
263
264    /// Flip the first byte of the on-disk object file for `h`, in place.
265    /// Shared by the `DisplaySource` corruption tests below — the same
266    /// "the bytes on disk no longer match `h`" setup the corruption tests
267    /// in `store.rs` use (deliberately duplicated there rather than
268    /// exposing a test-support seam).
269    fn corrupt_first_byte(store: &ObjectStore, h: &Hash, first_byte: u8) {
270        let path = store.path_for(h);
271        let mut f = OpenOptions::new()
272            .read(true)
273            .write(true)
274            .open(&path)
275            .unwrap();
276        f.seek(io::SeekFrom::Start(0)).unwrap();
277        f.write_all(&[first_byte ^ 0xFF]).unwrap();
278        f.sync_all().unwrap();
279    }
280
281    #[test]
282    fn display_source_delegates_to_unverified() {
283        // `DisplaySource` is the intended call-site adapter: wrapping the
284        // store must make `ObjectSource::read` succeed on an object that
285        // `ObjectStore::read` itself would reject.
286        let (_dir, store) = fresh_store();
287        let bytes = b"trustworthy".to_vec();
288        let h = store.write(&bytes).unwrap();
289        corrupt_first_byte(&store, &h, bytes[0]);
290        assert!(matches!(
291            store.read(&h).unwrap_err(),
292            StoreError::HashMismatch { .. }
293        ));
294
295        let display = DisplaySource::new(&store);
296        let mut corrupted = bytes.clone();
297        corrupted[0] ^= 0xFF;
298        assert_eq!(
299            display.read(&h).unwrap(),
300            corrupted,
301            "DisplaySource::read must delegate to the store's unverified read"
302        );
303    }
304
305    /// Regression test for the `EphemeralSink::read_object` override:
306    /// both a private-map (overlay) hit and a store fall-through must
307    /// decode a `Tree` correctly, and the store fall-through must still
308    /// surface `HashMismatch` on corruption — through `&dyn ObjectSource`,
309    /// the shape every `diff`/`status` caller actually uses.
310    #[test]
311    fn ephemeral_sink_read_object_tree_overlay_and_fallthrough() {
312        use crate::object::{EntryMode, Tree, TreeEntry};
313        let tree = |name: &[u8]| {
314            Object::Tree(Tree {
315                entries: vec![TreeEntry {
316                    name: name.to_vec(),
317                    mode: EntryMode::Blob,
318                    object_hash: crate::hash::hash(b"x"),
319                }],
320            })
321        };
322
323        let (_dir, store) = fresh_store();
324
325        // Store fall-through: object lives only in the durable store.
326        let durable_obj = tree(b"durable.txt");
327        let durable_bytes = serialize::serialize(&durable_obj).unwrap();
328        let durable_h = store.write(&durable_bytes).unwrap();
329
330        // Overlay hit: object lives only in the sink's private map.
331        let overlay_obj = tree(b"overlay.txt");
332        let overlay_bytes = serialize::serialize(&overlay_obj).unwrap();
333
334        let sink = EphemeralSink::new(&store);
335        let overlay_h = sink.put(&overlay_bytes).unwrap();
336
337        let via_trait: &dyn ObjectSource = &sink;
338        assert_eq!(via_trait.read_object(&durable_h).unwrap(), durable_obj);
339        assert_eq!(via_trait.read_object(&overlay_h).unwrap(), overlay_obj);
340
341        corrupt_first_byte(&store, &durable_h, durable_bytes[0]);
342        assert!(matches!(
343            via_trait.read_object(&durable_h).unwrap_err(),
344            StoreError::HashMismatch { .. }
345        ));
346    }
347
348    #[test]
349    fn ephemeral_sink_unverified_falls_through() {
350        // Two shapes an `EphemeralSink` reader can hit: a private-map
351        // object built by this snapshot (never touches disk, so nothing to
352        // corrupt) and a store-backed object corrupted on disk. Both must
353        // read fine through `DisplaySource<EphemeralSink>`.
354        let (_dir, store) = fresh_store();
355        let durable_bytes = b"trustworthy".to_vec();
356        let durable_h = store.write(&durable_bytes).unwrap();
357        corrupt_first_byte(&store, &durable_h, durable_bytes[0]);
358
359        let sink = EphemeralSink::new(&store);
360        let private_h = sink.put(b"snapshot-only").unwrap();
361
362        let display = DisplaySource::new(&sink);
363        assert_eq!(display.read(&private_h).unwrap(), b"snapshot-only");
364
365        let mut corrupted = durable_bytes.clone();
366        corrupted[0] ^= 0xFF;
367        assert_eq!(display.read(&durable_h).unwrap(), corrupted);
368    }
369
370    #[test]
371    fn read_unverified_default_is_verifying_read() {
372        // A minimal `ObjectSource` impl that only defines `read` must get
373        // fully-verifying behaviour from `read_unverified`'s default body
374        // — the whole safety net for third-party/future implementors that
375        // haven't opted in to the unverified path.
376        struct OnlyReadImpl<'s>(&'s ObjectStore);
377        impl ObjectSource for OnlyReadImpl<'_> {
378            fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
379                self.0.read(h)
380            }
381        }
382
383        let (_dir, store) = fresh_store();
384        let bytes = b"trustworthy".to_vec();
385        let h = store.write(&bytes).unwrap();
386        corrupt_first_byte(&store, &h, bytes[0]);
387
388        let only_read = OnlyReadImpl(&store);
389        assert!(matches!(
390            only_read.read_unverified(&h).unwrap_err(),
391            StoreError::HashMismatch { .. }
392        ));
393    }
394}