triblespace_core/repo.rs
1#![allow(clippy::type_complexity)]
2//! Content-addressed blob storage, complete capability proofs, collection
3//! records, durable wants, and read-only access to legacy named-pin snapshots.
4//!
5//! Collections are the mutable-history replacement. Legacy pin and commit
6//! encodings remain readable so existing piles can be migrated and retained,
7//! but this module deliberately exposes no API for mutating named pins.
8pub mod async_store;
9pub(crate) mod read_attempt;
10pub use read_attempt::ReadFailure;
11
12pub mod branch;
13/// Commit metadata construction and signature verification.
14pub mod commit;
15/// Storage adapter that delegates blobs and collection records to separate backends.
16pub mod hybridstore;
17/// Fully in-memory storage implementation for tests and ephemeral use.
18pub mod memoryrepo;
19#[cfg(feature = "object-store")]
20/// Storage backed by an `object_store`-compatible remote (S3, local FS, etc.).
21pub mod objectstore;
22/// Local file-based pile storage backend.
23pub mod pile;
24/// Grow-only native storage for complete capability proofs.
25pub mod proof;
26pub use crate::collection::observed_store::ObservedStore;
27pub use proof::{CapabilityProofRead, CapabilityProofStore};
28/// Generational collection of piles for lazy-retention blob storage.
29pub mod yard;
30
31/// Exact content absent from an immutable blob snapshot.
32///
33/// A live caller may acquire this handle and retry against a later snapshot;
34/// the failing snapshot itself never fetches or records demand.
35///
36/// The handle IS printed, and this was once deliberately mute. JP, 2026-09-21,
37/// on why it should not be: a program that already holds the handle in
38/// plaintext cannot leak it by printing it, because it is already on that
39/// machine. What the capability means is that a handle must not become
40/// available in plaintext to someone who does not already have it, either
41/// through a record or through a blob that contains it. That is a property of
42/// what you TRANSMIT and to whom, not of whether a value is formatted, and
43/// the one channel here that carries an error to a party who may hold neither
44/// -- a renderer showing a backend failure to a viewer -- is held by a
45/// separate finite error type that never receives this material.
46///
47/// It prints `blake3:<hex>` so the text can be pasted straight into
48/// `trible pile blob inspect`; that command answers `BadProtocol` for a bare
49/// hex string, which reads exactly like a missing blob and is not one.
50#[derive(Clone, Copy)]
51pub struct MissingBlob {
52 pub handle: Inline<Handle<UnknownBlob>>,
53}
54
55impl std::fmt::Display for MissingBlob {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(
58 f,
59 "blob blake3:{} is not resident in this snapshot",
60 hex::encode(self.handle.raw)
61 )
62 }
63}
64
65impl std::error::Error for MissingBlob {}
66
67/// Whether a backend error's source chain names unavailable exact bytes.
68/// Check before a diagnostic boundary converts the error into text.
69pub fn is_missing_blob(mut error: &(dyn std::error::Error + 'static)) -> bool {
70 loop {
71 if error.is::<MissingBlob>() {
72 return true;
73 }
74 match error.source() {
75 Some(source) => error = source,
76 None => return false,
77 }
78 }
79}
80
81impl std::fmt::Debug for MissingBlob {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 std::fmt::Display::fmt(self, f)
84 }
85}
86
87/// Trait for storage backends that require explicit close/cleanup.
88///
89/// Not all storage backends need to implement this; implementations that have
90/// nothing to do on close may return Ok(()) or use `Infallible` as the error
91/// type.
92pub trait StorageClose {
93 /// Error type returned by `close`.
94 type Error: std::error::Error;
95
96 /// Consume the storage and perform any necessary cleanup.
97 fn close(self) -> Result<(), Self::Error>;
98}
99
100/// Trait for storage backends that can make pending writes crash-durable.
101///
102/// Mirrors [`StorageClose`]: backends with buffered/unsynced state
103/// ([`pile::Pile`]'s appended records are not durable until
104/// [`pile::Pile::flush`]) expose it here so generic code can demand
105/// durability at a specific point — most importantly when recording a
106/// durable **want** whose writer may exit immediately afterwards (a
107/// faculty process recording a demand for a sync daemon to service).
108/// Backends with nothing to sync (in-memory stores) return `Ok(())` with
109/// `Infallible` as the error type.
110pub trait StorageFlush {
111 /// Error type returned by `flush`.
112 type Error: std::error::Error + Send + Sync + 'static;
113
114 /// Persist all pending writes and markers durably.
115 fn flush(&mut self) -> Result<(), Self::Error>;
116}
117
118/// Component mask for a changed store snapshot.
119///
120/// This is deliberately local invalidation evidence, not a portable revision
121/// or a promise that a component changed by only one element. It lets a
122/// consumer retain already-derived state for components that provably did not
123/// change while conservatively rebuilding everything for stores that cannot
124/// distinguish them.
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub struct StoreChanges(u8);
127
128impl StoreChanges {
129 /// No sync-visible component changed.
130 pub const NONE: Self = Self(0);
131 /// The observable blob view (membership, metadata, or retrievability) may
132 /// have changed.
133 pub const BLOBS: Self = Self(1 << 0);
134 /// Native collection records may have changed.
135 pub const COLLECTION_RECORDS: Self = Self(1 << 1);
136 /// Complete capability proofs may have changed.
137 pub const CAPABILITY_PROOFS: Self = Self(1 << 2);
138 /// Durable WANT requests may have changed.
139 pub const WANTS: Self = Self(1 << 3);
140 /// Every sync-visible component may have changed.
141 pub const ALL: Self = Self(
142 Self::BLOBS.0 | Self::COLLECTION_RECORDS.0 | Self::CAPABILITY_PROOFS.0 | Self::WANTS.0,
143 );
144
145 /// Whether every bit in `change` is present.
146 pub const fn contains(self, change: Self) -> bool {
147 self.0 & change.0 == change.0
148 }
149
150 /// Whether no sync-visible component changed.
151 pub const fn is_empty(self) -> bool {
152 self.0 == 0
153 }
154
155 /// Union two conservative change observations.
156 pub const fn union(self, other: Self) -> Self {
157 Self(self.0 | other.0)
158 }
159}
160
161/// The raw read-set of one retained immutable-store observation.
162///
163/// These are lookup interests, including absent records and blobs, rather
164/// than copies of their contents or a second catalog. A later matching record
165/// or physical blob occurrence can invalidate an earlier miss. Whole-component
166/// flags cover readers that enumerate rather than perform exact lookups.
167/// Query time and external provider availability are not stored dependencies;
168/// callers retain their existing deadline and exact-acquisition behavior.
169#[derive(Clone, Debug, Default, Eq, PartialEq)]
170pub struct StoreDependencies {
171 /// Raw record selection routes consulted by the observation.
172 pub records: BTreeSet<CollectionRecordSelector>,
173 /// Exact blob hashes consulted, whether resident, missing, or unreadable.
174 pub blobs: BTreeSet<CollectionData>,
175 /// Whether complete capability proof evidence was consulted.
176 pub capability_proofs: bool,
177 /// Whether an unrestricted record enumeration was consulted.
178 pub all_records: bool,
179 /// Whether an unrestricted blob enumeration was consulted.
180 pub all_blobs: bool,
181}
182
183impl StoreDependencies {
184 /// Whether no store component was consulted.
185 pub fn is_empty(&self) -> bool {
186 self.records.is_empty()
187 && self.blobs.is_empty()
188 && !self.capability_proofs
189 && !self.all_records
190 && !self.all_blobs
191 }
192}
193
194/// One immutable observation of a storage backend.
195///
196/// A snapshot is its own local revision token. It owns every read capability
197/// needed to interpret the prefix it observed, and compares directly with an
198/// earlier snapshot from the same store lineage. Cloning or reading never
199/// advances that observation.
200/// Record reads and residency observations are frozen and never perform work.
201/// A snapshot may additionally carry an asynchronous blob reader which fetches
202/// and caches exact immutable content without advancing these observations or
203/// recording durable demand. The default change classification is deliberately
204/// conservative for backends that cannot classify changes cheaply.
205pub trait StoreSnapshot: Clone + Send + Sync + 'static {
206 /// Conservatively classify changes since `previous`.
207 ///
208 /// False positives only repeat derived work. A false negative can strand
209 /// derived state, so implementations must report every component which may
210 /// have changed. This compares stored content only. Consumers caching
211 /// time-sensitive application queries must separately track their own
212 /// clocks and boundaries. Snapshots are local observations, not portable
213 /// versions.
214 fn changes_since(&self, _previous: &Self) -> StoreChanges {
215 StoreChanges::ALL
216 }
217
218 /// Conservatively classify changes relevant to a retained raw read-set.
219 ///
220 /// An unchanged result must cover misses as well as successful reads.
221 /// Backends may narrow comparison using shared indexes; this default only
222 /// masks [`changes_since`](Self::changes_since) by the components that
223 /// were consulted, so wrappers without scoped comparison remain safe.
224 /// Blob changes include additional physical occurrences of an existing
225 /// hash, which may recover a previously unreadable payload. WANT changes
226 /// and application clocks are deliberately outside this read-set.
227 fn changes_for(&self, previous: &Self, dependencies: &StoreDependencies) -> StoreChanges {
228 if dependencies.is_empty() {
229 return StoreChanges::NONE;
230 }
231 let observed = self.changes_since(previous);
232 let mut relevant = StoreChanges::NONE;
233 if (dependencies.all_records || !dependencies.records.is_empty())
234 && observed.contains(StoreChanges::COLLECTION_RECORDS)
235 {
236 relevant = relevant.union(StoreChanges::COLLECTION_RECORDS);
237 }
238 if (dependencies.all_blobs || !dependencies.blobs.is_empty())
239 && observed.contains(StoreChanges::BLOBS)
240 {
241 relevant = relevant.union(StoreChanges::BLOBS);
242 }
243 if dependencies.capability_proofs && observed.contains(StoreChanges::CAPABILITY_PROOFS) {
244 relevant = relevant.union(StoreChanges::CAPABILITY_PROOFS);
245 }
246 relevant
247 }
248}
249
250#[cfg(test)]
251mod missing_blob_tests {
252 use super::*;
253
254 /// The error names the handle it failed on, so a reader can act on it
255 /// rather than guess. A process that already holds the handle in plaintext
256 /// discloses nothing by printing it; the capability rule is about a handle
257 /// reaching someone who holds neither a record nor a blob containing it.
258 #[test]
259 fn missing_blob_names_the_handle_it_wanted() {
260 let raw = [0xABu8; INLINE_LEN];
261 let missing = MissingBlob {
262 handle: Inline::<Handle<UnknownBlob>>::new(raw),
263 };
264 let shown = missing.to_string();
265 assert!(
266 shown.contains(&hex::encode(raw)),
267 "the handle must appear in the message, got {shown:?}"
268 );
269 // Prefixed so the text pastes straight into `trible pile blob inspect`,
270 // which answers BadProtocol for a bare hex string.
271 assert!(
272 shown.contains("blake3:"),
273 "the handle must carry its scheme, got {shown:?}"
274 );
275 // Debug forwards to Display, so neither form is silently mute.
276 assert_eq!(format!("{missing:?}"), shown);
277 }
278}
279
280#[cfg(test)]
281mod store_dependency_tests {
282 use super::*;
283 use crate::collection::CollectionHandle;
284
285 #[derive(Clone)]
286 struct UnclassifiedSnapshot;
287
288 impl StoreSnapshot for UnclassifiedSnapshot {}
289
290 #[derive(Clone)]
291 struct ClassifiedSnapshot(StoreChanges);
292
293 impl StoreSnapshot for ClassifiedSnapshot {
294 fn changes_since(&self, _previous: &Self) -> StoreChanges {
295 self.0
296 }
297 }
298
299 #[test]
300 fn scoped_default_keeps_unclassified_wrappers_conservative() {
301 let snapshot = UnclassifiedSnapshot;
302 let mut dependencies = StoreDependencies::default();
303 assert!(dependencies.is_empty());
304 assert_eq!(
305 snapshot.changes_for(&snapshot, &dependencies),
306 StoreChanges::NONE
307 );
308
309 dependencies
310 .records
311 .insert(CollectionRecordSelector::Collection(CollectionHandle::new(
312 [1; INLINE_LEN],
313 )));
314 assert!(!dependencies.is_empty());
315 assert_eq!(
316 snapshot.changes_for(&snapshot, &dependencies),
317 StoreChanges::COLLECTION_RECORDS
318 );
319 dependencies
320 .blobs
321 .insert(CollectionData::new([2; INLINE_LEN]));
322 dependencies.capability_proofs = true;
323 let all_reads = StoreChanges::COLLECTION_RECORDS
324 .union(StoreChanges::BLOBS)
325 .union(StoreChanges::CAPABILITY_PROOFS);
326 assert_eq!(snapshot.changes_for(&snapshot, &dependencies), all_reads);
327 assert!(!all_reads.contains(StoreChanges::WANTS));
328
329 dependencies.records.clear();
330 dependencies.blobs.clear();
331 dependencies.all_records = true;
332 dependencies.all_blobs = true;
333 assert_eq!(snapshot.changes_for(&snapshot, &dependencies), all_reads);
334 }
335
336 #[test]
337 fn scoped_default_filters_only_uninterested_components() {
338 let before = ClassifiedSnapshot(StoreChanges::NONE);
339 let mut dependencies = StoreDependencies {
340 all_records: true,
341 ..StoreDependencies::default()
342 };
343 for observed in [StoreChanges::NONE, StoreChanges::BLOBS, StoreChanges::WANTS] {
344 assert_eq!(
345 ClassifiedSnapshot(observed).changes_for(&before, &dependencies),
346 StoreChanges::NONE
347 );
348 }
349 assert_eq!(
350 ClassifiedSnapshot(StoreChanges::ALL).changes_for(&before, &dependencies),
351 StoreChanges::COLLECTION_RECORDS
352 );
353 dependencies.capability_proofs = true;
354 assert_eq!(
355 ClassifiedSnapshot(StoreChanges::CAPABILITY_PROOFS).changes_for(&before, &dependencies),
356 StoreChanges::CAPABILITY_PROOFS
357 );
358 dependencies
359 .blobs
360 .insert(CollectionData::new([3; INLINE_LEN]));
361 assert_eq!(
362 ClassifiedSnapshot(StoreChanges::BLOBS).changes_for(&before, &dependencies),
363 StoreChanges::BLOBS
364 );
365 }
366}
367
368/// A mutable store which can freeze one immutable read observation.
369///
370/// Every semantic read capability implemented by a store shares this one
371/// associated snapshot. Records and proof evidence are frozen together.
372/// An attached asynchronous blob reader may resolve exact
373/// immutable handles later; that does not select a newer semantic observation.
374pub trait SnapshotSource {
375 /// Immutable observation returned by this store.
376 type Snapshot: StoreSnapshot;
377 /// Failure while refreshing and freezing an observation.
378 type SnapshotError: Error + Debug + Send + Sync + 'static;
379
380 /// Reobserve external changes and freeze the resulting known prefix.
381 fn snapshot(&mut self) -> Result<Self::Snapshot, Self::SnapshotError>;
382}
383
384/// Immutable snapshot type produced by `S`.
385pub type SnapshotOf<S> = <S as SnapshotSource>::Snapshot;
386
387/// Failure while freezing a snapshot of `S`.
388pub type SnapshotErrorOf<S> = <S as SnapshotSource>::SnapshotError;
389
390impl<S> SnapshotSource for &mut S
391where
392 S: SnapshotSource + ?Sized,
393{
394 type Snapshot = S::Snapshot;
395 type SnapshotError = S::SnapshotError;
396
397 fn snapshot(&mut self) -> Result<Self::Snapshot, Self::SnapshotError> {
398 (**self).snapshot()
399 }
400}
401
402impl<S> StorageFlush for &mut S
403where
404 S: StorageFlush + ?Sized,
405{
406 type Error = S::Error;
407
408 fn flush(&mut self) -> Result<(), Self::Error> {
409 (**self).flush()
410 }
411}
412
413use std::collections::{BTreeSet, HashSet, VecDeque};
414use std::convert::Infallible;
415use std::error::Error;
416use std::fmt::Debug;
417use std::fmt::{self};
418
419use crate::blob::encodings::UnknownBlob;
420use crate::blob::Blob;
421use crate::blob::BlobEncoding;
422use crate::blob::IntoBlob;
423use crate::collection::{
424 CollectionData, CollectionRead, CollectionRecordSelector, CollectionStore, CoverageRead,
425};
426use crate::inline::encodings::hash::Handle;
427use crate::inline::Inline;
428use crate::inline::InlineEncoding;
429use crate::inline::INLINE_LEN;
430use crate::patch::IdentitySchema;
431use crate::patch::PATCH;
432use crate::prelude::inlineencodings::GenId;
433use crate::trible::{TribleSet, V_END, V_START};
434
435use crate::blob::encodings::simplearchive::SimpleArchive;
436use crate::blob::encodings::utf8string::UTF8String;
437use crate::inline::encodings::shortstring::ShortString;
438use crate::prelude::*;
439
440attributes! {
441 /// The actual data of the commit.
442 "4DD4DDD05CC31734B03ABB4E43188B1F" unsafe as pub content: Handle<SimpleArchive>;
443 /// A commit that this commit is based on.
444 "317044B612C690000D798CA660ECFD2A" unsafe as pub parent: Handle<SimpleArchive>;
445 /// A (potentially long) message describing the commit.
446 "B59D147839100B6ED4B165DF76EDF3BB" unsafe as pub message: Handle<UTF8String>;
447 /// A short message describing the commit.
448 "12290C0BE0E9207E324F24DDE0D89300" unsafe as pub short_message: ShortString;
449 /// The hash of the first commit in the commit chain of the branch.
450 "272FBC56108F336C4D2E17289468C35F" unsafe as pub head: Handle<SimpleArchive>;
451 /// An id used to track the branch.
452 "8694CC73AF96A5E1C7635C677D1B928A" unsafe as pub branch: GenId;
453}
454
455/// Handle of a legacy signed commit metadata archive.
456pub type CommitHandle = Inline<Handle<SimpleArchive>>;
457
458/// Lightweight information returned while enumerating a blob store.
459///
460/// `length` is storage-observed metadata, not proof that the bytes decode to
461/// the encoding named by a subsequently typed handle. Consumers that accept a
462/// blob as data must still retrieve and validate it through [`BlobStoreGet`].
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub struct BlobInfo {
465 /// Content-addressed handle recorded by the store.
466 pub handle: Inline<Handle<UnknownBlob>>,
467 /// Stored payload length in bytes.
468 pub length: u64,
469}
470
471/// The `ListBlobs` trait is used to list all blobs in a repository.
472pub trait BlobStoreList {
473 /// Iterator over lightweight blob information in the store.
474 type Iter<'a>: Iterator<Item = Result<BlobInfo, Self::Err>>
475 where
476 Self: 'a;
477 /// Error type for listing operations.
478 type Err: Error + Debug + Send + Sync + 'static;
479
480 /// Lists all blobs in the repository.
481 fn blobs<'a>(&'a self) -> Self::Iter<'a>;
482
483 /// Test whether one blob is present in this store snapshot without
484 /// turning absence into demand.
485 ///
486 /// The default derives membership from [`blobs`](Self::blobs). Indexed
487 /// local readers should override it with their native lookup; wrappers
488 /// should delegate to the wrapped snapshot. Unlike [`BlobStoreGet::get`],
489 /// this is always an observation and must not record a want or fetch.
490 fn contains_blob<S>(&self, handle: Inline<Handle<S>>) -> Result<bool, Self::Err>
491 where
492 S: BlobEncoding + 'static,
493 Handle<S>: InlineEncoding,
494 {
495 for info in self.blobs() {
496 if info?.handle.raw == handle.raw {
497 return Ok(true);
498 }
499 }
500 Ok(false)
501 }
502
503 /// Return lightweight, unvalidated storage information for one resident
504 /// blob without reading or hashing its payload.
505 ///
506 /// This is an observation of the store index, just like
507 /// [`contains_blob`](Self::contains_blob). Consumers that use the payload
508 /// must still call [`BlobStoreGet::get`], which validates the recorded
509 /// content address. Indexed stores should override this method with their
510 /// native lookup; the default remains correct for list-only backends.
511 fn blob_info<S>(&self, handle: Inline<Handle<S>>) -> Result<Option<BlobInfo>, Self::Err>
512 where
513 S: BlobEncoding + 'static,
514 Handle<S>: InlineEncoding,
515 {
516 for info in self.blobs() {
517 let info = info?;
518 if info.handle.raw == handle.raw {
519 return Ok(Some(info));
520 }
521 }
522 Ok(None)
523 }
524
525 /// Lists blobs in `self` that are not in `old`.
526 ///
527 /// Backends with persistent indexes compute the difference cheaply via
528 /// their index's own set-difference operation. Backends without such an
529 /// index fall back to the default implementation, which lists all current
530 /// blobs — over-eager but always correct.
531 ///
532 /// Use this for "what blobs are new since I last looked" patterns
533 /// (e.g. announcing newly-imported blobs to a DHT) where holding the
534 /// previous snapshot as a baseline gives you the delta.
535 fn blobs_diff<'a>(&'a self, _old: &Self) -> Self::Iter<'a> {
536 self.blobs()
537 }
538}
539
540/// Metadata about a blob in a repository.
541#[derive(Debug, Clone)]
542pub struct BlobMetadata {
543 /// Timestamp in milliseconds since UNIX epoch when the blob was created/stored.
544 pub timestamp: u64,
545 /// Length of the blob in bytes.
546 pub length: u64,
547}
548
549/// Trait exposing metadata lookup for blobs available in a repository reader.
550pub trait BlobStoreMeta {
551 /// Error type returned by metadata calls.
552 type MetaError: std::error::Error + Send + Sync + 'static;
553
554 /// Returns metadata for the blob identified by `handle`, or `None` if
555 /// the blob is not present.
556 fn metadata<S>(
557 &self,
558 handle: Inline<Handle<S>>,
559 ) -> Result<Option<BlobMetadata>, Self::MetaError>
560 where
561 S: BlobEncoding + 'static,
562 Handle<S>: InlineEncoding;
563
564 /// The members of `handles` that are present in this store, as a set.
565 ///
566 /// Presence, not validity: a store answers this from its index, and the
567 /// bytes are validated where they are read. A store whose index projects
568 /// to the resident hashes answers with one intersection; the default
569 /// asks per handle.
570 fn resident(
571 &self,
572 handles: &PATCH<32, IdentitySchema, ()>,
573 ) -> Result<PATCH<32, IdentitySchema, ()>, Self::MetaError> {
574 let mut resident = PATCH::new();
575 for key in handles.iter_ordered() {
576 if self
577 .metadata(Inline::<Handle<UnknownBlob>>::new(*key))?
578 .is_some()
579 {
580 resident.insert(&crate::patch::Entry::new(key));
581 }
582 }
583 Ok(resident)
584 }
585}
586
587/// Trait exposing a monotonic "forget" operation.
588///
589/// Forget is idempotent and monotonic: it removes materialization from a
590/// particular repository but does not semantically delete derived facts.
591pub trait BlobStoreForget {
592 /// Error type for forget operations.
593 type ForgetError: std::error::Error + Send + Sync + 'static;
594
595 /// Removes the materialized blob identified by `handle` from this store.
596 fn forget<S>(&mut self, handle: Inline<Handle<S>>) -> Result<(), Self::ForgetError>
597 where
598 S: BlobEncoding + 'static,
599 Handle<S>: InlineEncoding;
600}
601
602/// The `GetBlob` trait is used to retrieve blobs from a repository.
603///
604/// Implementations are a trust boundary: on success, the returned typed value
605/// must carry `handle` as its content identity and its bytes must be the bytes
606/// validated for that identity. Callers may trust the cached handle and must
607/// not need to hash the same bytes again. Stores ingesting untrusted pile,
608/// object-store, or network data therefore perform content-address validation
609/// before constructing the returned [`Blob`].
610pub trait BlobStoreGet {
611 /// Error type for get operations, parameterised by the deserialization error.
612 type GetError<E: std::error::Error + Send + Sync + 'static>: Error + Send + Sync + 'static;
613
614 /// Retrieves a blob from the repository by its handle.
615 /// The handle is a unique identifier for the blob, and is used to retrieve it from the repository.
616 /// The blob is returned as a [`Blob`] object, which contains the raw bytes of the blob,
617 /// which can be deserialized via the appropriate schema type, which is specified by the `T` type parameter.
618 ///
619 /// # Errors
620 /// Returns an error if the blob could not be found in the repository.
621 /// The error type is specified by the `Err` associated type.
622 fn get<T, S>(
623 &self,
624 handle: Inline<Handle<S>>,
625 ) -> Result<T, Self::GetError<<T as TryFromBlob<S>>::Error>>
626 where
627 S: BlobEncoding + 'static,
628 T: TryFromBlob<S>,
629 Handle<S>: InlineEncoding;
630}
631
632/// The `PutBlob` trait is used to store blobs in a repository.
633pub trait BlobStorePut {
634 /// Error type for put operations.
635 type PutError: Error + Debug + Send + Sync + 'static;
636
637 /// Serialises `item` as a blob, stores it, and returns its handle.
638 fn put<S, T>(&mut self, item: T) -> Result<Inline<Handle<S>>, Self::PutError>
639 where
640 S: BlobEncoding + 'static,
641 T: IntoBlob<S>,
642 Handle<S>: InlineEncoding;
643}
644
645impl<B> BlobStorePut for &mut B
646where
647 B: BlobStorePut + ?Sized,
648{
649 type PutError = B::PutError;
650
651 fn put<S, T>(&mut self, item: T) -> Result<Inline<Handle<S>>, Self::PutError>
652 where
653 S: BlobEncoding + 'static,
654 T: IntoBlob<S>,
655 Handle<S>: InlineEncoding,
656 {
657 (**self).put(item)
658 }
659}
660
661/// Combined blob storage whose reads come from the store's one shared
662/// immutable snapshot.
663///
664/// Blob writes remain a property of the mutable store. Blob reads and listings
665/// are properties of [`SnapshotSource::Snapshot`], so collection admission,
666/// capability verification, and payload decoding can all observe one prefix.
667pub trait BlobStore: BlobStorePut + SnapshotSource<Snapshot: BlobStoreGet + BlobStoreList> {}
668
669impl<B> BlobStore for B
670where
671 B: BlobStorePut + SnapshotSource + ?Sized,
672 B::Snapshot: BlobStoreGet + BlobStoreList,
673{
674}
675
676/// Immutable read surface of a complete repository snapshot.
677pub trait StoreRead:
678 StoreSnapshot
679 + BlobStoreGet
680 + BlobStoreList
681 + BlobStoreMeta
682 + CollectionRead
683 + CoverageRead
684 + CapabilityProofRead
685 + WantRead
686{
687}
688
689impl<R> StoreRead for R where
690 R: StoreSnapshot
691 + BlobStoreGet
692 + BlobStoreList
693 + BlobStoreMeta
694 + CollectionRead
695 + CoverageRead
696 + CapabilityProofRead
697 + WantRead
698{
699}
700
701/// Mutable repository whose semantic reads all share one snapshot.
702pub trait Store:
703 BlobStore + CollectionStore + CapabilityProofStore + SnapshotSource<Snapshot: StoreRead>
704{
705}
706
707impl<S> Store for S
708where
709 S: BlobStore + CollectionStore + CapabilityProofStore + SnapshotSource,
710 S::Snapshot: StoreRead,
711{
712}
713
714/// Trait for blob stores that can retain a supplied set of handles.
715pub trait BlobStoreKeep {
716 /// Retain only the blobs identified by `handles`.
717 fn keep<I>(&mut self, handles: I)
718 where
719 I: IntoIterator<Item = Inline<Handle<UnknownBlob>>>;
720}
721
722/// Explicit roots for one retention pass.
723///
724/// Retention has two different edge meanings which must not be conflated:
725///
726/// - **direct** roots retain exactly the named blob and do not inspect its
727/// payload; and
728/// - **recursive** roots own their resident descendants, discovered through
729/// [`reachable`].
730///
731/// These caller-selected roots supplement native-record ownership. Every
732/// retained collection record and WANT owns its resident direct references
733/// recursively; backends discover those edges separately from this explicit
734/// policy value and without semantic admission. A capability proof owns its
735/// resident capability definitions (not its opaque resource identity), and a
736/// retired capability proof, read no further than its structure, owns every
737/// resident blob any 32 bytes of it name.
738#[derive(Clone, Debug, Default, Eq, PartialEq)]
739pub struct RetentionRoots {
740 direct: BTreeSet<[u8; INLINE_LEN]>,
741 recursive: BTreeSet<[u8; INLINE_LEN]>,
742}
743
744impl RetentionRoots {
745 /// Construct an empty retention policy result.
746 pub fn new() -> Self {
747 Self::default()
748 }
749
750 /// Retain exactly `handle`, without interpreting its bytes as ownership
751 /// edges.
752 pub fn retain_direct<S>(&mut self, handle: Inline<Handle<S>>)
753 where
754 S: BlobEncoding + 'static,
755 Handle<S>: InlineEncoding,
756 {
757 self.direct.insert(handle.raw);
758 }
759
760 /// Retain `handle` and every resident descendant reached through the
761 /// store's conservative child traversal.
762 pub fn retain_recursive<S>(&mut self, handle: Inline<Handle<S>>)
763 where
764 S: BlobEncoding + 'static,
765 Handle<S>: InlineEncoding,
766 {
767 self.recursive.insert(handle.raw);
768 }
769
770 /// Merge another policy result into this one.
771 pub fn union(&mut self, other: Self) {
772 self.direct.extend(other.direct);
773 self.recursive.extend(other.recursive);
774 }
775
776 /// Direct roots in deterministic handle order.
777 pub fn direct(&self) -> impl ExactSizeIterator<Item = Inline<Handle<UnknownBlob>>> + '_ {
778 self.direct
779 .iter()
780 .copied()
781 .map(Inline::<Handle<UnknownBlob>>::new)
782 }
783
784 /// Recursive roots in deterministic handle order.
785 pub fn recursive(&self) -> impl ExactSizeIterator<Item = Inline<Handle<UnknownBlob>>> + '_ {
786 self.recursive
787 .iter()
788 .copied()
789 .map(Inline::<Handle<UnknownBlob>>::new)
790 }
791
792 /// Expand this policy against a reader into the exact resident keep set.
793 ///
794 /// Direct roots are inserted without calling [`BlobChildren::children`].
795 /// Recursive roots use [`reachable`], whose existing missing-child
796 /// behavior remains conservative for the blobs that are locally present.
797 pub fn expanded<R>(&self, reader: &R) -> BTreeSet<Inline<Handle<UnknownBlob>>>
798 where
799 R: BlobChildren,
800 {
801 let mut keep: BTreeSet<_> = self.direct().collect();
802 keep.extend(reachable(reader, self.recursive()));
803 keep
804 }
805
806 /// Whether neither kind of root is present.
807 pub fn is_empty(&self) -> bool {
808 self.direct.is_empty() && self.recursive.is_empty()
809 }
810}
811
812/// Trait for stores that can enumerate a blob's child references.
813///
814/// "Children" are the 32-byte-aligned values in a blob that correspond
815/// to existing blobs in the store — the conservative set of references.
816///
817/// The default implementation scans the blob's bytes and checks each
818/// 32-byte chunk with [`BlobStoreGet::get`]. Backends with batch
819/// capabilities (e.g. a network store with a SYNC protocol) can
820/// override this for efficiency.
821pub trait BlobChildren: BlobStoreGet {
822 /// Return handles of blobs referenced by `handle` that exist in this store.
823 fn children(&self, handle: Inline<Handle<UnknownBlob>>) -> Vec<Inline<Handle<UnknownBlob>>> {
824 let Ok(blob) = self.get::<Blob<UnknownBlob>, UnknownBlob>(handle) else {
825 return Vec::new();
826 };
827 let bytes = blob.bytes.as_ref();
828 let mut result = Vec::new();
829 let mut offset = 0usize;
830 while offset + INLINE_LEN <= bytes.len() {
831 let mut raw = [0u8; INLINE_LEN];
832 raw.copy_from_slice(&bytes[offset..offset + INLINE_LEN]);
833 let candidate = Inline::<Handle<UnknownBlob>>::new(raw);
834 if self.get::<anybytes::Bytes, UnknownBlob>(candidate).is_ok() {
835 result.push(candidate);
836 }
837 offset += INLINE_LEN;
838 }
839 result
840 }
841}
842
843// No blanket impl — types opt in explicitly so they can provide
844// optimized implementations (e.g. network stores with batch protocols).
845// Use `impl_blob_children_default!` for the scan-and-check fallback.
846
847/// A point-in-time snapshot of (pin id → head) mappings.
848///
849/// PATCH keyed by 16-byte pin id, valued by the pinned head's handle.
850/// This type exists only for explicit legacy pile migration and diagnosis;
851/// current serving paths use collection records directly.
852///
853/// Returned by [`PinSnapshotSource::snapshot_pin_heads`].
854pub type PinSnapshot = PATCH<16, IdentitySchema, Inline<Handle<SimpleArchive>>>;
855
856/// Observational access to one point-in-time snapshot of pin heads.
857///
858/// Explicit migration and diagnosis tools can inspect legacy pile state
859/// without regaining piecemeal access or compare-and-swap mutation. The
860/// returned snapshot remains fully inspectable. The mutable receiver permits
861/// [`crate::repo::pile::Pile`] to refresh externally appended records before
862/// producing the snapshot; the capability itself is read-only and is not
863/// forwarded by current storage composition wrappers.
864///
865/// Implementations must return a complete snapshot or an error. Listing or
866/// per-head failures must never be hidden by returning a partial view.
867pub trait PinSnapshotSource {
868 /// Error returned when a stable pin-head snapshot cannot be produced.
869 type PinSnapshotError: Error + Debug + Send + Sync + 'static;
870
871 /// Return a point-in-time snapshot of every `(pin id, head)` mapping.
872 fn snapshot_pin_heads(&mut self) -> Result<PinSnapshot, Self::PinSnapshotError>;
873}
874
875#[cfg(test)]
876mod pin_snapshot_source_tests {
877 use super::PinSnapshotSource;
878 use crate::repo::pile::Pile;
879
880 fn assert_source<T: PinSnapshotSource>() {}
881
882 #[test]
883 fn only_pile_exposes_legacy_pin_inspection() {
884 assert_source::<Pile>();
885 }
886}
887
888/// Exact byte length of a canonical [`WantRequest`].
889pub const WANT_REQUEST_BYTES_LEN: usize = 1 + 3 * INLINE_LEN;
890
891/// Versioned tag of a blob request in the canonical [`WantRequest`] codec.
892pub const WANT_REQUEST_KIND_BLOB_V1: u8 = 1;
893/// Retired derive tag, seen only in historical pile WANT logs. It encoded
894/// `(source, target, input)`; a later tag 4 dropped the source, and then
895/// computation wants were removed altogether. Kept so a decoder can name
896/// what it refuses to interpret.
897#[cfg(test)]
898pub(crate) const WANT_REQUEST_KIND_DERIVE_V1: u8 = 3;
899
900/// A durable request for absent content.
901///
902/// A request names only what it wants, and a fulfiller satisfies it by
903/// fetching the content. Requests for *work* -- "someone compute this merge
904/// or derivation" -- were once a second shape here and are gone: a blob want
905/// costs the wanter the bytes, so a flood floods itself, while a computation
906/// want costs the wanter nothing and asks others to pay; whoever wants a
907/// computation runs it.
908#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
909pub enum WantRequest {
910 /// Obtain and retain one content-addressed blob according to local policy.
911 Blob {
912 /// Type-erased bearer handle used for local lookup or global discovery.
913 handle: Inline<Handle<UnknownBlob>>,
914 },
915}
916
917impl WantRequest {
918 /// Construct a blob request from any typed content handle.
919 pub fn blob<S>(handle: Inline<Handle<S>>) -> Self
920 where
921 S: BlobEncoding + 'static,
922 Handle<S>: InlineEncoding,
923 {
924 Self::Blob {
925 handle: handle.transmute(),
926 }
927 }
928
929 /// Return the requested blob handle, if this is an exact-content request.
930 pub const fn blob_handle(self) -> Option<Inline<Handle<UnknownBlob>>> {
931 match self {
932 Self::Blob { handle } => Some(handle),
933 }
934 }
935
936 /// Blob handles named directly by this durable request.
937 ///
938 /// WANT is an ordinary structural lifetime record: when retained it owns
939 /// whichever of these handles are resident, recursively. The method does
940 /// not fetch absent bytes or turn them into subordinate wants.
941 pub fn blob_references(self) -> impl ExactSizeIterator<Item = Inline<Handle<UnknownBlob>>> {
942 let mut references = arrayvec::ArrayVec::<_, 3>::new();
943 match self {
944 Self::Blob { handle } => references.push(handle),
945 }
946 references.into_iter()
947 }
948
949 /// Encode this request into its exact tagged 97-byte representation.
950 pub fn to_bytes(self) -> [u8; WANT_REQUEST_BYTES_LEN] {
951 let mut bytes = [0; WANT_REQUEST_BYTES_LEN];
952 match self {
953 Self::Blob { handle } => {
954 bytes[0] = WANT_REQUEST_KIND_BLOB_V1;
955 write_want_field(&mut bytes, 0, handle.raw);
956 }
957 }
958 bytes
959 }
960
961 /// Decode one exact canonical 97-byte request.
962 pub fn from_bytes(bytes: [u8; WANT_REQUEST_BYTES_LEN]) -> Result<Self, WantRequestDecodeError> {
963 match bytes[0] {
964 WANT_REQUEST_KIND_BLOB_V1 => {
965 if bytes[1 + INLINE_LEN..].iter().any(|byte| *byte != 0) {
966 return Err(WantRequestDecodeError::NonZeroUnusedFields {
967 kind: WANT_REQUEST_KIND_BLOB_V1,
968 });
969 }
970 Ok(Self::Blob {
971 handle: Inline::new(read_want_field(&bytes, 0)),
972 })
973 }
974 unknown => Err(WantRequestDecodeError::UnknownKind(unknown)),
975 }
976 }
977}
978
979/// Structural failure while decoding a canonical [`WantRequest`].
980#[derive(Clone, Copy, Debug, Eq, PartialEq)]
981pub enum WantRequestDecodeError {
982 /// The versioned variant tag is unknown.
983 UnknownKind(u8),
984 /// A short variant used non-zero bytes in a reserved field.
985 NonZeroUnusedFields { kind: u8 },
986}
987
988impl fmt::Display for WantRequestDecodeError {
989 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
990 match self {
991 Self::UnknownKind(kind) => {
992 write!(formatter, "want request has unknown dense kind {kind}")
993 }
994 Self::NonZeroUnusedFields { kind } => write!(
995 formatter,
996 "want request kind {kind} has non-zero unused fields"
997 ),
998 }
999 }
1000}
1001
1002impl Error for WantRequestDecodeError {}
1003
1004fn write_want_field(
1005 bytes: &mut [u8; WANT_REQUEST_BYTES_LEN],
1006 index: usize,
1007 field: [u8; INLINE_LEN],
1008) {
1009 let start = 1 + index * INLINE_LEN;
1010 bytes[start..start + INLINE_LEN].copy_from_slice(&field);
1011}
1012
1013fn read_want_field(bytes: &[u8; WANT_REQUEST_BYTES_LEN], index: usize) -> [u8; INLINE_LEN] {
1014 let start = 1 + index * INLINE_LEN;
1015 bytes[start..start + INLINE_LEN]
1016 .try_into()
1017 .expect("validated fixed-width want request field")
1018}
1019
1020/// Immutable enumeration of one frozen durable-WANT observation.
1021pub trait WantRead {
1022 /// Error while enumerating the frozen request set.
1023 type WantsError: Error + Debug + Send + Sync + 'static;
1024
1025 /// Iterator over the frozen grow-only request set.
1026 type WantIter<'a>: Iterator<Item = Result<WantRequest, Self::WantsError>>
1027 where
1028 Self: 'a;
1029
1030 /// List the request set captured by this observation.
1031 fn wants<'a>(&'a self) -> Result<Self::WantIter<'a>, Self::WantsError>;
1032}
1033
1034/// Insertion capability for durable blob wants.
1035///
1036/// Wants are an idempotent grow-only set, independent of legacy named-pin
1037/// evidence and native collection records. A backend may support either
1038/// capability without supporting the other. Repeated [`want`](Self::want)
1039/// calls for one exact request have no additional effect. Frozen enumeration
1040/// is exposed separately through [`WantRead`]. Exact-content requests are
1041/// identified solely by their bearer handle.
1042///
1043/// Forgetting is deliberately not a counter-record operation. A physical
1044/// rewrite may omit a WANT deliberately, but while the record is retained it
1045/// strongly owns every independently resident direct blob reference. No
1046/// appended negative fact may retract another replica's demand after pile
1047/// concatenation.
1048pub trait WantStore {
1049 /// Error type for want operations.
1050 type WantError: Error + Debug + Send + Sync + 'static;
1051
1052 /// Add durable interest in `request` idempotently.
1053 fn want(&mut self, request: WantRequest) -> Result<(), Self::WantError>;
1054}
1055
1056#[cfg(test)]
1057mod want_request_tests {
1058 use super::*;
1059
1060 #[test]
1061 fn typed_blob_request_roundtrips_with_zero_unused_fields() {
1062 let typed = Inline::<Handle<SimpleArchive>>::new([0x41; INLINE_LEN]);
1063 let request = WantRequest::blob(typed);
1064 let bytes = request.to_bytes();
1065
1066 assert_eq!(bytes.len(), WANT_REQUEST_BYTES_LEN);
1067 assert_eq!(bytes[0], WANT_REQUEST_KIND_BLOB_V1);
1068 assert!(bytes[1 + INLINE_LEN..].iter().all(|byte| *byte == 0));
1069 assert_eq!(WantRequest::from_bytes(bytes), Ok(request));
1070 assert_eq!(
1071 request,
1072 WantRequest::Blob {
1073 handle: typed.transmute()
1074 }
1075 );
1076 }
1077
1078 #[test]
1079 fn wants_enumerate_every_direct_blob_reference() {
1080 let blob = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([1; INLINE_LEN]));
1081
1082 assert_eq!(
1083 blob.blob_references()
1084 .map(|handle| handle.raw)
1085 .collect::<Vec<_>>(),
1086 vec![[1; INLINE_LEN]],
1087 );
1088 }
1089
1090 #[test]
1091 fn dense_decoder_rejects_noncanonical_shapes() {
1092 let mut unknown = [0; WANT_REQUEST_BYTES_LEN];
1093 unknown[0] = 99;
1094 assert_eq!(
1095 WantRequest::from_bytes(unknown),
1096 Err(WantRequestDecodeError::UnknownKind(99))
1097 );
1098
1099 let mut padded =
1100 WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([0x51; INLINE_LEN])).to_bytes();
1101 padded[1 + INLINE_LEN] = 1;
1102 assert_eq!(
1103 WantRequest::from_bytes(padded),
1104 Err(WantRequestDecodeError::NonZeroUnusedFields {
1105 kind: WANT_REQUEST_KIND_BLOB_V1,
1106 })
1107 );
1108 }
1109}
1110
1111/// Error returned by [`transfer`] when copying blobs between stores.
1112#[derive(Debug)]
1113pub enum TransferError<ListErr, LoadErr, StoreErr> {
1114 /// Failed to list handles from the source.
1115 List(ListErr),
1116 /// Failed to load a blob from the source.
1117 Load(LoadErr),
1118 /// Failed to store a blob in the target.
1119 Store(StoreErr),
1120}
1121
1122impl<ListErr, LoadErr, StoreErr> fmt::Display for TransferError<ListErr, LoadErr, StoreErr> {
1123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1124 write!(f, "failed to transfer blob")
1125 }
1126}
1127
1128impl<ListErr, LoadErr, StoreErr> Error for TransferError<ListErr, LoadErr, StoreErr>
1129where
1130 ListErr: Debug + Error + 'static,
1131 LoadErr: Debug + Error + 'static,
1132 StoreErr: Debug + Error + 'static,
1133{
1134 fn source(&self) -> Option<&(dyn Error + 'static)> {
1135 match self {
1136 Self::List(e) => Some(e),
1137 Self::Load(e) => Some(e),
1138 Self::Store(e) => Some(e),
1139 }
1140 }
1141}
1142
1143/// Copies the specified blob handles from `source` into `target`.
1144pub fn transfer<'a, BS, BT, Handles>(
1145 source: &'a BS,
1146 target: &'a mut BT,
1147 handles: Handles,
1148) -> impl Iterator<
1149 Item = Result<
1150 (Inline<Handle<UnknownBlob>>, Inline<Handle<UnknownBlob>>),
1151 TransferError<
1152 Infallible,
1153 <BS as BlobStoreGet>::GetError<Infallible>,
1154 <BT as BlobStorePut>::PutError,
1155 >,
1156 >,
1157> + 'a
1158where
1159 BS: BlobStoreGet + 'a,
1160 BT: BlobStorePut + 'a,
1161 Handles: IntoIterator<Item = Inline<Handle<UnknownBlob>>> + 'a,
1162 Handles::IntoIter: 'a,
1163{
1164 handles.into_iter().map(move |source_handle| {
1165 let blob: Blob<UnknownBlob> = source.get(source_handle).map_err(TransferError::Load)?;
1166
1167 Ok((
1168 source_handle,
1169 (target.put(blob).map_err(TransferError::Store)?),
1170 ))
1171 })
1172}
1173
1174/// Iterator that visits every blob handle reachable from a set of roots.
1175///
1176/// Uses [`BlobChildren`] to enumerate references at each level,
1177/// so backends with batch capabilities get efficient traversal.
1178pub struct ReachableHandles<'a, BS>
1179where
1180 BS: BlobChildren,
1181{
1182 source: &'a BS,
1183 queue: VecDeque<Inline<Handle<UnknownBlob>>>,
1184 visited: HashSet<[u8; INLINE_LEN]>,
1185}
1186
1187impl<'a, BS> ReachableHandles<'a, BS>
1188where
1189 BS: BlobChildren,
1190{
1191 fn new(source: &'a BS, roots: impl IntoIterator<Item = Inline<Handle<UnknownBlob>>>) -> Self {
1192 let mut queue = VecDeque::new();
1193 for handle in roots {
1194 queue.push_back(handle);
1195 }
1196
1197 Self {
1198 source,
1199 queue,
1200 visited: HashSet::new(),
1201 }
1202 }
1203}
1204
1205impl<'a, BS> Iterator for ReachableHandles<'a, BS>
1206where
1207 BS: BlobChildren,
1208{
1209 type Item = Inline<Handle<UnknownBlob>>;
1210
1211 fn next(&mut self) -> Option<Self::Item> {
1212 while let Some(handle) = self.queue.pop_front() {
1213 let raw = handle.raw;
1214
1215 if !self.visited.insert(raw) {
1216 continue;
1217 }
1218
1219 // Use BlobChildren to get references — backends can override
1220 // with batch-optimized implementations.
1221 for child in self.source.children(handle) {
1222 if !self.visited.contains(&child.raw) {
1223 self.queue.push_back(child);
1224 }
1225 }
1226
1227 return Some(handle);
1228 }
1229
1230 None
1231 }
1232}
1233
1234/// Create a breadth-first iterator over blob handles reachable from `roots`.
1235///
1236/// Uses [`BlobChildren`] for reference enumeration, so network-backed
1237/// stores can provide optimized batch implementations.
1238pub fn reachable<'a, BS>(
1239 source: &'a BS,
1240 roots: impl IntoIterator<Item = Inline<Handle<UnknownBlob>>>,
1241) -> ReachableHandles<'a, BS>
1242where
1243 BS: BlobChildren,
1244{
1245 ReachableHandles::new(source, roots)
1246}
1247
1248/// Iterate over every 32-byte candidate in the value column of a [`TribleSet`].
1249///
1250/// This is a conservative conversion used when scanning metadata for potential
1251/// blob handles. Each 32-byte chunk is treated as a `Handle<UnknownBlob>`.
1252/// Callers can feed the resulting iterator into [`BlobStoreKeep::keep`] or other
1253/// helpers that accept collections of handles.
1254pub fn potential_handles<'a>(
1255 set: &'a TribleSet,
1256) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> + 'a {
1257 set.vae.iter().map(|raw| {
1258 let mut value = [0u8; INLINE_LEN];
1259 value.copy_from_slice(&raw[V_START..=V_END]);
1260 Inline::<Handle<UnknownBlob>>::new(value)
1261 })
1262}