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}