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}