vole_document/store/mod.rs
1//! Content-addressed object store (Phase 9).
2//!
3//! A `.voldoc` descriptor's object table is a single ordered sequence. Each
4//! entry is either an inline byte object (`OBJECT`, tag `0x10`) or a reference
5//! to an object held by an [`ObjectStore`] (`EXTERNAL_REF`, tag `0x80`). Inline
6//! and external objects share one id space, so equal bytes are equal ids
7//! regardless of which document they came from.
8//!
9//! ## Two digests, two roles
10//!
11//! | digest | role | authority |
12//! |---|---|---|
13//! | SHA-256 | whole reconstructed source; archival identity | parse/materialize/verify |
14//! | BLAKE3-256 ([`Id`]) | one object's bytes; store namespace | relational/advisory |
15//!
16//! An [`Id`] is an *ephemeral, relational* name for a shareable object. It is
17//! never a substitute for the source digest and never appears in `INTEGRITY`.
18//!
19//! ## Verification rule
20//!
21//! [`ObjectStore::get`] **must** re-hash the returned bytes and reject with
22//! [`crate::ErrorClass::IntegrityMismatch`] unless `BLAKE3(bytes) == id`.
23//! [`ObjectStore::get_range`] cannot re-verify the whole object; exactness for a
24//! store-backed materialization therefore rests on the `EXTERNAL_REF` declared
25//! length and the descriptor's `INTEGRITY` SHA-256 checked by `materialize`.
26
27use core::fmt;
28use std::collections::BTreeSet;
29
30use crate::container::{Descriptor, ObjectSource};
31use crate::error::{Error, Result};
32
33mod io;
34pub use io::{IoCounters, IoSnapshot};
35
36// Phase 23: directory-durability (GAP 1) and the optional barrier journal
37// (GAP 2). Used by the field store's atomic writers (seed / index / packed /
38// descriptor / manifest / cache); gated on the `field` feature for the same
39// reason they are.
40#[cfg(feature = "field")]
41pub mod durable;
42#[cfg(feature = "field")]
43pub use durable::{DirSyncPolicy, set_dir_sync_policy};
44
45#[cfg(feature = "store")]
46mod embedded;
47#[cfg(feature = "store")]
48pub use embedded::{EmbeddedStore, STORE_FORMAT_VERSION, STORE_MAGIC};
49
50#[cfg(feature = "store")]
51mod account;
52#[cfg(feature = "store")]
53pub use account::{AccountReport, RootAccount, account};
54
55#[cfg(feature = "entropyfs-store")]
56mod entropyfs;
57#[cfg(feature = "entropyfs-store")]
58pub use entropyfs::EntropyFsStore;
59// The engine-error mapper and the engine-backed seed methods are used only by the
60// field backend; gating the re-export on both features avoids an unused import in
61// an `entropyfs-store`-without-`field` build.
62#[cfg(all(feature = "entropyfs-store", feature = "field"))]
63pub(crate) use entropyfs::map_engine_error;
64
65#[cfg(feature = "field")]
66mod seed;
67#[cfg(feature = "field")]
68pub use seed::{
69 FsSeedStore, NodeId, SEED_FORMAT_VERSION, SEED_NODE_DOMAIN, SeedStore, SeedStoreStats,
70 closure as seed_closure,
71};
72
73#[cfg(feature = "field")]
74mod pack;
75#[cfg(feature = "field")]
76pub use pack::{PackedSeedStore, SyncPolicy};
77
78/// Size accounting reported by a backend.
79///
80/// `total_bytes` is the sum of raw unique-object lengths (the backend-independent
81/// headline); `stored_bytes` is what the backend physically writes (equal to
82/// `total_bytes` for [`EmbeddedStore`], which stores raw; smaller for a
83/// compressing backend).
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
85pub struct StoreStats {
86 /// Unique ids present.
87 pub object_count: u64,
88 /// Sum of object file lengths (raw).
89 pub total_bytes: u64,
90 /// On-disk bytes (`== total_bytes` for a raw backend).
91 pub stored_bytes: u64,
92}
93
94/// Outcome of a mark-and-sweep GC pass.
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
96pub struct GcReport {
97 /// Number of distinct external ids reachable from the roots.
98 pub reachable: u64,
99 /// Number of stored objects removed.
100 pub swept: u64,
101 /// Raw bytes reclaimed by the sweep.
102 pub bytes_reclaimed: u64,
103 /// Reachable ids the store does not contain (must be empty for a valid
104 /// closure; a non-empty list is a live `MissingExternalObject`).
105 pub dangling: Vec<Id>,
106}
107
108/// `Id` is a newtype over the 32 raw bytes of `BLAKE3-256(object_bytes)`, with no
109/// domain prefix. Hex is lower-case.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
111pub struct Id([u8; 32]);
112
113impl Id {
114 /// Wrap 32 raw digest bytes.
115 pub const fn from_bytes(b: [u8; 32]) -> Self {
116 Id(b)
117 }
118
119 /// The raw digest bytes.
120 pub const fn as_bytes(&self) -> &[u8; 32] {
121 &self.0
122 }
123
124 /// Content id of `bytes`: `BLAKE3-256(bytes)`.
125 #[cfg(feature = "store")]
126 pub fn of(bytes: &[u8]) -> Self {
127 Id(*blake3::hash(bytes).as_bytes())
128 }
129
130 /// Lower-case hex rendering (64 characters).
131 pub fn to_hex(&self) -> String {
132 crate::integrity::to_hex(&self.0)
133 }
134
135 /// Parse exactly 64 lower- or upper-case hex characters.
136 pub fn from_hex(s: &str) -> Result<Self> {
137 let raw = s.as_bytes();
138 if raw.len() != 64 {
139 return Err(Error::usage(format!(
140 "object id must be 64 hex characters, got {}",
141 raw.len()
142 )));
143 }
144 let nib = |c: u8| -> Option<u8> {
145 match c {
146 b'0'..=b'9' => Some(c - b'0'),
147 b'a'..=b'f' => Some(c - b'a' + 10),
148 b'A'..=b'F' => Some(c - b'A' + 10),
149 _ => None,
150 }
151 };
152 let mut out = [0u8; 32];
153 for (i, byte) in out.iter_mut().enumerate() {
154 let hi =
155 nib(raw[2 * i]).ok_or_else(|| Error::usage("object id has a non-hex character"))?;
156 let lo = nib(raw[2 * i + 1])
157 .ok_or_else(|| Error::usage("object id has a non-hex character"))?;
158 *byte = (hi << 4) | lo;
159 }
160 Ok(Id(out))
161 }
162}
163
164impl fmt::Display for Id {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(&self.to_hex())
167 }
168}
169
170/// A content-addressed store keyed by `BLAKE3-256` of the object bytes.
171///
172/// The object namespace is the descriptor's object table: the raw, uncompressed
173/// bytes a DRA `EMIT_OBJECT`/`DEFLATE_REPLAY` op consumes. Inline and external
174/// objects share one id space, so equal bytes are equal ids regardless of which
175/// document they came from.
176///
177/// `put` takes `&mut self` (ids are content-derived, so retries are idempotent).
178/// `list`/`remove` support mark-and-sweep GC; both decline by default so a
179/// backend that exposes no enumeration or per-object delete (EntropyFS) fails
180/// closed rather than reporting a fake sweep.
181pub trait ObjectStore {
182 /// Store `bytes`, returning its content id.
183 ///
184 /// Idempotent and at-least-once: identical bytes always return the same id
185 /// and are stored once; a retry after a crash is a no-op. A returned id
186 /// guarantees the bytes are durable under [`ObjectStore::get`].
187 fn put(&mut self, bytes: &[u8]) -> Result<Id>;
188
189 /// Fetch the exact bytes for `id`.
190 ///
191 /// MUST verify `BLAKE3(bytes) == id` before returning.
192 /// - absent id -> [`crate::ErrorClass::MissingExternalObject`]
193 /// - bytes hash != id -> [`crate::ErrorClass::IntegrityMismatch`]
194 fn get(&self, id: &Id) -> Result<Vec<u8>>;
195
196 /// Fetch `len` bytes of `id` starting at `offset`.
197 ///
198 /// STRICT: a request for `offset + len > stored_len(id)` is a typed error,
199 /// never a silent EOF clip. Range reads carry no whole-object hash gate.
200 fn get_range(&self, id: &Id, offset: u64, len: u64) -> Result<Vec<u8>>;
201
202 /// Whether `id` is present. `EmbeddedStore` overrides it with a stat.
203 fn contains(&self, id: &Id) -> Result<bool> {
204 let _ = id;
205 Err(Error::unsupported_feature(
206 "contains is not implemented by this backend",
207 ))
208 }
209
210 /// Every stored `(id, raw_len)`, for closure checking and GC.
211 ///
212 /// The default declines, so a backend without enumeration never reports a
213 /// fake closure.
214 fn list(&self) -> Result<Vec<(Id, u64)>> {
215 Err(Error::unsupported_feature(
216 "object enumeration is not implemented by this backend",
217 ))
218 }
219
220 /// Delete `id`, returning the raw bytes reclaimed (0 if absent).
221 ///
222 /// The default declines: a backend that exposes no per-object delete must
223 /// not appear to have swept anything.
224 fn remove(&self, id: &Id) -> Result<u64> {
225 let _ = id;
226 Err(Error::unsupported_feature(
227 "per-object delete is not implemented by this backend",
228 ))
229 }
230}
231
232/// The resolver consumed by store-backed materialization. A blanket impl makes
233/// every [`ObjectStore`] a resolver, so callers pass `&store`.
234pub trait ObjectResolver {
235 /// Fetch `id`, verifying that its length is exactly `len`.
236 fn get(&self, id: &Id, len: u64) -> Result<Vec<u8>>;
237}
238
239impl<T: ObjectStore> ObjectResolver for T {
240 fn get(&self, id: &Id, len: u64) -> Result<Vec<u8>> {
241 let bytes = ObjectStore::get(self, id)?;
242 if bytes.len() as u64 != len {
243 return Err(Error::integrity_mismatch(format!(
244 "external object {id} has {} bytes, EXTERNAL_REF declared {len}",
245 bytes.len()
246 )));
247 }
248 Ok(bytes)
249 }
250}
251
252/// A resolver that resolves nothing; used by the standalone `materialize`.
253pub struct NullResolver;
254
255impl ObjectResolver for NullResolver {
256 fn get(&self, id: &Id, _len: u64) -> Result<Vec<u8>> {
257 Err(Error::missing_external_object(format!(
258 "no store supplied to resolve object {id}"
259 )))
260 }
261}
262
263/// Inline → external: put every object's bytes into `store`, replacing it with
264/// an [`ObjectSource::External`] reference.
265///
266/// Objects already external are resolved through `resolver` (so a reference the
267/// resolver cannot satisfy fails closed) and re-put into `store`, so every
268/// object ends up in the destination store. Identical objects collapse to one
269/// store entry by content addressing (refcount > 1 is one stored object).
270pub fn externalize<R: ObjectResolver + ?Sized>(
271 d: &mut Descriptor,
272 resolver: &R,
273 store: &mut impl ObjectStore,
274) -> Result<()> {
275 let mut out: Vec<ObjectSource> = Vec::with_capacity(d.objects.len());
276 for src in &d.objects {
277 let bytes: Vec<u8> = match src {
278 ObjectSource::Inline(bytes) => bytes.clone(),
279 ObjectSource::External { id, len } => resolver.get(id, *len)?,
280 };
281 let len = bytes.len() as u64;
282 let id = store.put(&bytes)?;
283 out.push(ObjectSource::External { id, len });
284 }
285 d.objects = out;
286 Ok(())
287}
288
289/// External → inline: resolve every reference through `resolver` (verifying id
290/// and length) and replace it with [`ObjectSource::Inline`]. The external
291/// feature bit clears automatically because `required_features()` is derived.
292pub fn hydrate<R: ObjectResolver>(d: &mut Descriptor, resolver: &R) -> Result<()> {
293 let mut out: Vec<ObjectSource> = Vec::with_capacity(d.objects.len());
294 for src in &d.objects {
295 match src {
296 ObjectSource::Inline(bytes) => out.push(ObjectSource::Inline(bytes.clone())),
297 ObjectSource::External { id, len } => {
298 out.push(ObjectSource::Inline(resolver.get(id, *len)?));
299 }
300 }
301 }
302 d.objects = out;
303 Ok(())
304}
305
306/// Mark-and-sweep garbage collection over a set of root descriptors.
307///
308/// *Mark*: the union of every root's external ids. *Sweep*: `stored \ mark`.
309/// `dangling` is `mark \ stored` and must be empty for a valid closure.
310pub fn gc<R: ObjectResolver + ObjectStore>(roots: &[Descriptor], store: &R) -> Result<GcReport> {
311 let mut mark: BTreeSet<Id> = BTreeSet::new();
312 for root in roots {
313 for src in &root.objects {
314 if let ObjectSource::External { id, .. } = src {
315 mark.insert(*id);
316 }
317 }
318 }
319
320 let stored = store.list()?;
321 let stored_ids: BTreeSet<Id> = stored.iter().map(|(id, _)| *id).collect();
322
323 let mut dangling: Vec<Id> = mark
324 .iter()
325 .filter(|id| !stored_ids.contains(id))
326 .copied()
327 .collect();
328 dangling.sort_unstable();
329
330 let mut swept: u64 = 0;
331 let mut bytes_reclaimed: u64 = 0;
332 for (id, _len) in &stored {
333 if !mark.contains(id) {
334 bytes_reclaimed += store.remove(id)?;
335 swept += 1;
336 }
337 }
338
339 Ok(GcReport {
340 reachable: mark.len() as u64,
341 swept,
342 bytes_reclaimed,
343 dangling,
344 })
345}