Skip to main content

triblespace_core/
blob.rs

1//! Anything that can be represented as a byte sequence.
2//!
3//! Blobs store larger data items outside tribles and values. For the design
4//! rationale and an extended usage example see the [Blobs
5//! chapter](../book/src/deep-dive/blobs.md) of the Tribles Book.
6
7// Converting Rust types to blobs is infallible in practice, so only `IntoBlob`
8// and `TryFromBlob` are used throughout the codebase.  `TryToBlob` and
9// `FromBlob` were never required and have been removed for simplicity.
10
11mod cache;
12/// Built-in blob encoding types and their conversion implementations.
13pub mod encodings;
14/// Opaque discovery locators which do not disclose bearer blob handles.
15pub mod locator;
16mod memoryblobstore;
17
18use crate::inline::encodings::hash::Handle;
19use crate::inline::Inline;
20use crate::inline::InlineEncoding;
21use crate::metadata::MetaDescribe;
22
23use std::convert::Infallible;
24use std::error::Error;
25use std::fmt::Debug;
26use std::fmt::{self};
27use std::hash::Hash;
28use std::marker::PhantomData;
29
30/// Re-export of the blob cache wrapper.
31pub use cache::BlobCache;
32/// Re-export of the in-memory blob store.
33pub use memoryblobstore::{MemoryBlobStore, MemoryBlobStoreSnapshot, MemoryStoreGetError};
34
35/// Re-export of `anybytes::Bytes` for blob payloads.
36pub use anybytes::Bytes;
37
38/// A content-addressed value: immutable bytes paired with their
39/// Blake3 handle and a schema marker.
40///
41/// `Blob<S>` is the **heavy form** of a content-addressed payload —
42/// it carries the bytes plus the cached
43/// [`Inline<Handle<S>>`][Handle] that names them. The handle is the
44/// **lightweight form**: a 32-byte reference you can store in
45/// tribles, send across the network, or hand around freely without
46/// dragging the bytes along. `Blob` ↔ `Handle<S>` is the same
47/// "content / reference" duality as `Vec<T>` ↔ `&[T]`, except the
48/// reference is hash-based rather than pointer-based and survives
49/// crossing process boundaries.
50///
51/// The link is enforced by construction:
52/// - [`Blob::new`] hashes the bytes and stores the resulting handle.
53///   Subsequent `get_handle` / `as_ref` calls are O(1).
54/// - [`Blob::with_handle`] is the explicit "trust me" constructor for
55///   read paths where the handle is already known (a blob-store
56///   reader pulling a known-keyed entry, a pile-format decoder where
57///   the index has the hash). Caller asserts `handle == Blake3(bytes)`.
58/// - [`Blob::transmute`] / [`Blob::as_transmute`] preserve the cached
59///   handle across schema casts — the Blake3 hash is over bytes, not
60///   over schema, so the digest survives the phantom change.
61///
62/// `Blob<S>: AsRef<Inline<Handle<S>>>` so `&blob` deref-coerces to the
63/// lightweight reference for free.
64///
65/// The previous shape (`#[repr(transparent)]` around `Bytes`) was
66/// given up deliberately: caching the handle in the struct
67/// eliminates a real double-hash that surfaced at every `insert` site,
68/// and the only call that relied on transparency (`as_transmute`'s
69/// `mem::transmute`) still works because `Blob<S>` and `Blob<T>`
70/// have identical layouts for any `S`/`T: BlobEncoding` (phantoms
71/// are zero-sized, handle is `[u8; 32] + PhantomData`).
72pub struct Blob<S: BlobEncoding> {
73    /// The raw byte content of this blob.
74    pub bytes: Bytes,
75    /// Cached content-addressed handle. Computed eagerly at
76    /// construction time; reused on every `get_handle` call and on
77    /// `MemoryBlobStore::insert`.
78    handle: Inline<Handle<S>>,
79    _schema: PhantomData<S>,
80}
81
82impl<S> Blob<S>
83where
84    S: BlobEncoding,
85    Handle<S>: InlineEncoding,
86{
87    /// Creates a new blob from a sequence of bytes.
88    ///
89    /// **Hashes eagerly**: this call runs Blake3 over `bytes` once and
90    /// caches the resulting handle. Subsequent `get_handle` /
91    /// `MemoryBlobStore::insert` calls reuse the cached value at O(1).
92    /// For most use cases this is what callers want — `Blob::new`
93    /// almost always precedes an `insert` or a `get_handle`. If you
94    /// have a blob path that's *never* hashed and the eager cost
95    /// matters, reach for the raw `Bytes` instead.
96    pub fn new(bytes: Bytes) -> Self {
97        let digest = crate::inline::encodings::hash::Blake3::digest(&bytes);
98        Self {
99            bytes,
100            handle: Inline::new(digest),
101            _schema: PhantomData,
102        }
103    }
104
105    /// Constructs a blob from bytes *and* a precomputed handle,
106    /// skipping the hash step.
107    ///
108    /// Used by blob-store snapshots (`MemoryBlobStoreSnapshot::get` and
109    /// friends) and pile-format decoders that already know the
110    /// handle the blob is stored under — they read the bytes out of
111    /// their backing storage already keyed by hash, so recomputing
112    /// it would be pure overhead.
113    ///
114    /// # Safety
115    ///
116    /// The caller asserts that `handle == Blake3(bytes)`. The cache
117    /// is trusted on read paths; if these diverge,
118    /// `MemoryBlobStore::insert(blob)` will store the bytes under
119    /// `handle` (not the true Blake3 hash), and subsequent lookups
120    /// will silently miss or return wrong data. Always pair this
121    /// with a hash you got from a trusted source (the same store
122    /// you're reading from, the pile header, a verified network
123    /// fetch). For callers without that guarantee, use
124    /// [`Blob::new`] which hashes from bytes.
125    pub fn with_handle(bytes: Bytes, handle: Inline<Handle<S>>) -> Self {
126        Self {
127            bytes,
128            handle,
129            _schema: PhantomData,
130        }
131    }
132
133    /// Reinterprets the contained bytes as a blob of a different schema.
134    ///
135    /// This is a zero-copy transformation: bytes pass through and the
136    /// cached handle is recast at the phantom level. It does **not**
137    /// validate that the data actually conforms to the new schema.
138    pub fn transmute<T: BlobEncoding>(self) -> Blob<T>
139    where
140        Handle<T>: InlineEncoding,
141    {
142        Blob {
143            bytes: self.bytes,
144            handle: self.handle.transmute(),
145            _schema: PhantomData,
146        }
147    }
148
149    /// Transmutes the blob to a blob of a different schema.
150    /// This is a zero-cost operation.
151    /// If the schema types are not compatible, this will not cause undefined behavior,
152    /// but it might cause unexpected results.
153    ///
154    /// This is primarily used to give blobs with an [UnknownBlob](crate::blob::encodings::UnknownBlob) schema a more specific schema.
155    /// Use with caution.
156    pub fn as_transmute<T: BlobEncoding>(&self) -> &Blob<T> {
157        unsafe { std::mem::transmute(self) }
158    }
159
160    /// Returns the cached Blake3 handle. O(1) — no rehash.
161    ///
162    /// The handle is the *lightweight reference* form of this blob —
163    /// 32 bytes you can store in a trible, share over the network, or
164    /// pass around freely. The blob is the *heavy* form (bytes you
165    /// can decode). Both share the same Blake3 identity.
166    pub fn get_handle(&self) -> Inline<Handle<S>> {
167        self.handle
168    }
169
170    /// Tries to convert the blob to a concrete Rust type.
171    /// If the conversion fails, an error is returned.
172    pub fn try_from_blob<T>(self) -> Result<T, <T as TryFromBlob<S>>::Error>
173    where
174        T: TryFromBlob<S>,
175    {
176        <T as TryFromBlob<S>>::try_from_blob(self)
177    }
178}
179
180impl<T> Clone for Blob<T>
181where
182    T: BlobEncoding,
183    Handle<T>: InlineEncoding,
184{
185    fn clone(&self) -> Self {
186        Self {
187            bytes: self.bytes.clone(),
188            handle: self.handle,
189            _schema: PhantomData,
190        }
191    }
192}
193
194/// `Blob<S>` borrows as the `Inline<Handle<S>>` that references it.
195///
196/// Models the heavy/lightweight duality at the type system level:
197/// a `Blob<S>` IS a content-addressed value, and its `Handle<S>` is
198/// the 32-byte reference form. Coercing a `&Blob<S>` to a
199/// `&Inline<Handle<S>>` is free — the handle is stored as a field —
200/// so code that wants to pass the lightweight reference around
201/// (e.g. inserting into a trible, sending over the network) can
202/// just `blob.as_ref()` instead of `&blob.get_handle()`.
203impl<S> AsRef<Inline<Handle<S>>> for Blob<S>
204where
205    S: BlobEncoding,
206    Handle<S>: InlineEncoding,
207{
208    fn as_ref(&self) -> &Inline<Handle<S>> {
209        &self.handle
210    }
211}
212
213impl<T: BlobEncoding> PartialEq for Blob<T> {
214    fn eq(&self, other: &Self) -> bool {
215        self.bytes == other.bytes
216    }
217}
218
219impl<T: BlobEncoding> Eq for Blob<T> {}
220
221impl<T: BlobEncoding> Hash for Blob<T> {
222    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
223        self.bytes.hash(state);
224    }
225}
226
227impl<T: BlobEncoding> Debug for Blob<T> {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        write!(f, "Blob<{}>", std::any::type_name::<T>())
230    }
231}
232
233/// A trait for defining the abstract schema type of a blob.
234/// This is similar to the [`InlineEncoding`] trait in the [`inline`](crate::inline) module.
235pub trait BlobEncoding: MetaDescribe + Sized + 'static {
236    /// Converts a concrete Rust type to a blob with this schema via [`IntoBlob`].
237    fn blob_from<T: IntoBlob<Self>>(t: T) -> Blob<Self> {
238        t.to_blob()
239    }
240
241    /// Lift a `Blob<Self>` into the [`Encoded`](crate::inline::Encoded)
242    /// sum `entity!{}` consumes — yields
243    /// `Encoded::Blob(blob.transmute())`. The handle lives inside the
244    /// blob; consumers recover it via
245    /// [`Encoded::inline`](crate::inline::Encoded::inline).
246    ///
247    /// Overridable if a schema has unusual storage semantics. The
248    /// inline-path counterpart lives on
249    /// [`InlineEncoding::to_encoded`].
250    fn to_encoded(blob: Blob<Self>) -> crate::inline::Encoded<Handle<Self>>
251    where
252        Handle<Self>: InlineEncoding,
253    {
254        crate::inline::Encoded::Blob(blob.transmute::<crate::blob::encodings::UnknownBlob>())
255    }
256}
257
258/// Shorthand bound for `IntoEncoded<S, Output = Blob<S>>` — "this
259/// source produces a `Blob<S>` for content-addressed storage."
260///
261/// `IntoBlob` is a supertrait alias over
262/// [`IntoEncoded`](crate::inline::IntoEncoded): any type that
263/// implements `IntoEncoded<S>` with `Output = Blob<S>` automatically
264/// becomes `IntoBlob<S>`, and gains the `to_blob(self) -> Blob<S>`
265/// convenience method.
266///
267/// The trait parameter is the [`BlobEncoding`] directly (not
268/// `Handle<S>`) — this is what makes `impl IntoBlob<MyBlobEncoding>
269/// for MyForeignType` legal for downstream crates: the local
270/// `MyBlobEncoding` sits at trait position 0, satisfying Rust's
271/// orphan rule.
272pub trait IntoBlob<S: BlobEncoding>: crate::inline::IntoEncoded<S, Output = Blob<S>> {
273    /// Convert directly to `Blob<S>`.
274    fn to_blob(self) -> Blob<S>
275    where
276        Self: Sized,
277    {
278        self.into_encoded()
279    }
280}
281impl<S, T> IntoBlob<S> for T
282where
283    S: BlobEncoding,
284    T: crate::inline::IntoEncoded<S, Output = Blob<S>>,
285{
286}
287
288/// A trait for converting a [Blob] with a specific schema to a Rust type.
289/// This trait is implemented on the concrete Rust type.
290///
291/// This might return an error if the conversion is not possible,
292/// This is the counterpart to the [`IntoBlob`] trait.
293///
294/// See [TryFromInline](crate::inline::TryFromInline) for the counterpart trait for values.
295pub trait TryFromBlob<S: BlobEncoding>: Sized {
296    /// The error type returned when the conversion fails.
297    type Error: Error + Send + Sync + 'static;
298    /// Attempts to convert a blob into this type.
299    fn try_from_blob(b: Blob<S>) -> Result<Self, Self::Error>;
300}
301
302impl<S: BlobEncoding> TryFromBlob<S> for Blob<S> {
303    type Error = Infallible;
304
305    fn try_from_blob(b: Blob<S>) -> Result<Self, Self::Error> {
306        Ok(b)
307    }
308}
309
310/// `Blob<S>` is the identity source for [`IntoEncoded<S>`](crate::inline::IntoEncoded) in the
311/// blob path: it converts to itself with no allocation, and the
312/// cached handle inside lets every downstream step skip rehashing.
313impl<S: BlobEncoding> crate::inline::Encodes<Blob<S>> for S
314where
315    Handle<S>: InlineEncoding,
316{
317    type Output = Blob<S>;
318    fn encode(source: Blob<S>) -> Blob<S> {
319        source
320    }
321}
322
323/// `Blob<T>` is the `ToEncoded<Handle<T>>` expander: it delegates to
324/// [`BlobEncoding::to_encoded`] for the actual blob-to-Encoded lift. The
325/// trait is the macro-side dispatch shim; the logic lives on
326/// `BlobEncoding` so users (and schemas that need custom storage
327/// semantics) can call or override it directly.
328impl<T> crate::inline::ToEncoded<Handle<T>> for Blob<T>
329where
330    T: BlobEncoding,
331    Handle<T>: InlineEncoding,
332{
333    fn to_encoded(self) -> crate::inline::Encoded<Handle<T>> {
334        <T as BlobEncoding>::to_encoded(self)
335    }
336}
337
338/// Precomputed-handle case: a `Inline<Handle<T>>` can be passed as a
339/// `IntoEncoded<T>` source (T is the BlobEncoding, matching the
340/// `Handle<T>`-attributed field's `Encoding`). Output is the value
341/// itself; no side-blob — caller asserts the bytes live somewhere
342/// resolvable.
343impl<T: BlobEncoding> crate::inline::Encodes<Inline<Handle<T>>> for T
344where
345    Handle<T>: InlineEncoding,
346{
347    type Output = Inline<Handle<T>>;
348    fn encode(source: Inline<Handle<T>>) -> Inline<Handle<T>> {
349        source
350    }
351}
352
353/// Reference form of the precomputed-handle case.
354impl<T: BlobEncoding> crate::inline::Encodes<&Inline<Handle<T>>> for T
355where
356    Handle<T>: InlineEncoding,
357{
358    type Output = Inline<Handle<T>>;
359    fn encode(source: &Inline<Handle<T>>) -> Inline<Handle<T>> {
360        *source
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::blob::encodings::UnknownBlob;
368    use crate::inline::encodings::hash::Blake3;
369
370    #[test]
371    fn new_computes_and_caches_handle() {
372        let b: Blob<UnknownBlob> = Blob::new(Bytes::from(b"hello".to_vec()));
373        let h1 = b.get_handle();
374        let h2 = b.get_handle();
375        // Same handle on repeat — cache is stable.
376        assert_eq!(h1, h2);
377        // And matches a fresh independent Blake3 of the bytes.
378        let independent = Inline::new(Blake3::digest(b"hello"));
379        let h_typed: Inline<Handle<UnknownBlob>> = independent;
380        assert_eq!(h1, h_typed);
381    }
382
383    #[test]
384    fn with_handle_trusts_the_provided_handle() {
385        // Construct a blob with a *deliberately bogus* handle. The
386        // cache returns it verbatim — proving we don't recompute from
387        // bytes. This is the optimization read paths exploit (they
388        // already know the handle, no point re-hashing).
389        let bogus: Inline<Handle<UnknownBlob>> = Inline::new([0xAA; 32]);
390        let b: Blob<UnknownBlob> = Blob::with_handle(Bytes::from(b"any bytes".to_vec()), bogus);
391        assert_eq!(b.get_handle(), bogus);
392    }
393
394    #[test]
395    fn as_ref_borrows_the_lightweight_handle() {
396        let b: Blob<UnknownBlob> = Blob::new(Bytes::from(b"borrow me".to_vec()));
397        let h_owned: Inline<Handle<UnknownBlob>> = b.get_handle();
398        let h_borrowed: &Inline<Handle<UnknownBlob>> = b.as_ref();
399        // Same value, no allocation, no rehash.
400        assert_eq!(h_owned, *h_borrowed);
401    }
402
403    #[test]
404    fn transmute_carries_cached_handle() {
405        let b: Blob<UnknownBlob> = Blob::new(Bytes::from(b"shared".to_vec()));
406        let h_before: Inline<Handle<UnknownBlob>> = b.get_handle();
407        // Schema cast — handle bytes stay identical, only the phantom
408        // changes.
409        let b2: Blob<crate::blob::encodings::utf8string::UTF8String> =
410            b.transmute::<crate::blob::encodings::utf8string::UTF8String>();
411        let h_after = b2.get_handle();
412        assert_eq!(h_before.raw, h_after.raw);
413    }
414}