Skip to main content

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}