1pub mod build;
16pub mod cache;
17pub mod capabilities;
18pub mod coarse;
19pub mod dag;
20pub mod derive;
21pub mod document_format;
22pub mod edit;
23pub mod explain;
24pub mod external;
25pub mod index;
26pub mod incremental;
31pub(crate) mod inflate;
34pub mod ingest;
35#[cfg(feature = "package")]
36pub mod ingest_package;
37pub mod manifest;
38pub mod node;
39pub mod observe;
40#[cfg(feature = "opc")]
41pub mod opc;
42pub mod partial;
43pub mod plan;
44pub(crate) mod prof;
45pub mod promote;
46pub mod provenance;
47pub mod resource;
48pub mod session;
49pub mod share;
50
51pub use manifest::{FieldId, FieldRoot};
52
53pub(crate) fn warm_prof_enabled() -> bool {
57 std::env::var_os("VOLE_PROFILE_OPEN").is_some()
58}
59
60#[cfg(feature = "phase28-3-ablation")]
68pub(crate) fn selective_serve_enabled() -> bool {
69 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
70 *ON.get_or_init(|| std::env::var_os("VOLE_SERVE_SELECTIVE").is_some())
71}
72
73use std::fs;
74use std::path::{Path, PathBuf};
75use std::rc::Rc;
76
77#[cfg(feature = "entropyfs-store")]
78use std::sync::Arc;
79
80#[cfg(feature = "entropyfs-store")]
81use entropyfs::engine::BlobId;
82
83use crate::container::ParsedDescriptor;
84use crate::error::{Error, Result};
85use crate::limits::Limits;
86#[cfg(feature = "entropyfs-store")]
87use crate::store::{EntropyFsStore, map_engine_error};
88use crate::store::{
89 FsSeedStore, Id, IoCounters, IoSnapshot, NodeId, PackedSeedStore, SeedStore, SyncPolicy,
90};
91
92#[cfg(feature = "entropyfs-store")]
93use self::manifest::FIELD_ROOT_DOMAIN;
94use self::node::{NodeKind, SeedNode};
95
96pub const FIELD_UNIVERSE: &str = "vole-document;universe;phase11;exact-bytes;dra-8;opaque+entropy+pdf+channels+offsets+packed+packed-channels+deflate-replay-preflate-0.7.6-experimental+observation-index-v1+seek-directory-v1+external-objects-v1+procedural-seed-field-v1+hier-index-v1";
98
99#[cfg(feature = "package")]
106pub const PACKAGE_UNIVERSE: &str = "vole-document;universe;phase11;exact-bytes;dra-8;opaque+entropy+pdf+channels+offsets+packed+packed-channels+deflate-replay-preflate-0.7.6-experimental+observation-index-v1+seek-directory-v1+external-objects-v1+procedural-seed-field-v1+hier-index-v1+package-v1";
107
108#[derive(Clone)]
117pub(crate) enum SeedSubstrate {
118 Fs { root: PathBuf, io: IoCounters },
120 Packed { store: Rc<PackedSeedStore> },
122 #[cfg(feature = "entropyfs-store")]
124 EntropyFs {
125 store: Arc<EntropyFsStore>,
126 io: IoCounters,
127 },
128}
129
130impl SeedStore for SeedSubstrate {
131 fn put_node(&mut self, canonical: &[u8]) -> Result<NodeId> {
132 match self {
133 SeedSubstrate::Fs { root, io } => {
134 FsSeedStore::open_with_io(root, io.handle())?.put_node(canonical)
135 }
136 SeedSubstrate::Packed { store } => store.insert(canonical),
137 #[cfg(feature = "entropyfs-store")]
138 SeedSubstrate::EntropyFs { store, .. } => store.seed_put(canonical),
139 }
140 }
141
142 fn get_node(&self, id: &NodeId) -> Result<Vec<u8>> {
143 match self {
144 SeedSubstrate::Fs { root, io } => {
145 FsSeedStore::open_with_io(root, io.handle())?.get_node(id)
146 }
147 SeedSubstrate::Packed { store } => store.fetch(id),
148 #[cfg(feature = "entropyfs-store")]
149 SeedSubstrate::EntropyFs { store, io } => {
150 let bytes = store.seed_get(id)?;
151 io.add_seed(bytes.len() as u64);
152 Ok(bytes)
153 }
154 }
155 }
156
157 fn get_node_range(&self, id: &NodeId, offset: u64, len: u64) -> Result<Vec<u8>> {
158 match self {
159 SeedSubstrate::Fs { root, io } => {
160 FsSeedStore::open_with_io(root, io.handle())?.get_node_range(id, offset, len)
161 }
162 SeedSubstrate::Packed { store } => store.fetch_range(id, offset, len),
163 #[cfg(feature = "entropyfs-store")]
164 SeedSubstrate::EntropyFs { store, io } => {
165 let bytes = store.seed_get_range(id, offset, len)?;
166 io.add_seed(bytes.len() as u64);
167 Ok(bytes)
168 }
169 }
170 }
171
172 fn contains_node(&self, id: &NodeId) -> Result<bool> {
173 match self {
174 SeedSubstrate::Fs { root, io } => {
175 FsSeedStore::open_with_io(root, io.handle())?.contains_node(id)
176 }
177 SeedSubstrate::Packed { store } => store.has(id),
178 #[cfg(feature = "entropyfs-store")]
179 SeedSubstrate::EntropyFs { store, .. } => store.seed_contains(id),
180 }
181 }
182
183 fn list_nodes(&self) -> Result<Vec<(NodeId, u64)>> {
184 match self {
185 SeedSubstrate::Fs { root, io } => {
186 FsSeedStore::open_with_io(root, io.handle())?.list_nodes()
187 }
188 SeedSubstrate::Packed { store } => store.entries(),
189 #[cfg(feature = "entropyfs-store")]
190 SeedSubstrate::EntropyFs { store, .. } => store.seed_list(),
191 }
192 }
193}
194
195enum BlobBackend {
203 Fs,
205 #[cfg(feature = "entropyfs-store")]
211 EntropyFs(Arc<EntropyFsStore>),
212}
213
214impl BlobBackend {
215 fn descriptor_put(&self, root: &Path, bytes: &[u8]) -> Result<Id> {
216 match self {
217 BlobBackend::Fs => {
218 let id = Id::of(bytes);
219 let path = root.join("descriptor").join(id.to_hex());
220 if !path.exists() {
221 write_atomic(&path, bytes)?;
222 }
223 Ok(id)
224 }
225 #[cfg(feature = "entropyfs-store")]
226 BlobBackend::EntropyFs(store) => {
227 let id = Id::of(bytes);
228 let blob = store
229 .engine()
230 .put_blob(bytes)
231 .map_err(|e| map_engine_error("put_blob", &e))?;
232 debug_assert_eq!(blob.as_bytes(), id.as_bytes());
233 Ok(id)
234 }
235 }
236 }
237
238 fn descriptor_get(&self, root: &Path, id: &Id) -> Result<Vec<u8>> {
239 match self {
240 BlobBackend::Fs => {
241 let path = root.join("descriptor").join(id.to_hex());
242 fs::read(&path).map_err(|e| {
243 if e.kind() == std::io::ErrorKind::NotFound {
244 Error::missing_external_object(format!(
245 "descriptor blob {id} is not present"
246 ))
247 } else {
248 Error::io(format!("reading descriptor blob {id}: {e}"))
249 }
250 })
251 }
252 #[cfg(feature = "entropyfs-store")]
253 BlobBackend::EntropyFs(store) => store
254 .engine()
255 .get_blob(BlobId::new(*id.as_bytes()))
256 .map_err(|e| map_engine_error("get_blob", &e)),
257 }
258 }
259
260 fn field_put(&self, root: &Path, manifest: &FieldRoot) -> Result<FieldId> {
261 let bytes = manifest.encode_canonical();
262 let id = FieldId::of_manifest(&bytes);
263 match self {
264 BlobBackend::Fs => {
265 let path = root.join("field").join(id.to_hex());
266 if !path.exists() {
267 write_atomic(&path, &bytes)?;
268 }
269 Ok(id)
270 }
271 #[cfg(feature = "entropyfs-store")]
272 BlobBackend::EntropyFs(store) => {
273 let mut prefixed = Vec::with_capacity(FIELD_ROOT_DOMAIN.len() + bytes.len());
274 prefixed.extend_from_slice(FIELD_ROOT_DOMAIN);
275 prefixed.extend_from_slice(&bytes);
276 let blob = store
277 .engine()
278 .put_blob(&prefixed)
279 .map_err(|e| map_engine_error("put_blob", &e))?;
280 debug_assert_eq!(blob.as_bytes(), id.as_bytes());
281 Ok(id)
282 }
283 }
284 }
285
286 fn field_get(&self, root: &Path, id: &FieldId) -> Result<Vec<u8>> {
287 match self {
288 BlobBackend::Fs => {
289 let path = root.join("field").join(id.to_hex());
290 fs::read(&path).map_err(|e| {
291 if e.kind() == std::io::ErrorKind::NotFound {
292 Error::missing_external_object(format!("field {id} is not present"))
293 } else {
294 Error::io(format!("reading field {id}: {e}"))
295 }
296 })
297 }
298 #[cfg(feature = "entropyfs-store")]
299 BlobBackend::EntropyFs(store) => {
300 let blob = store
301 .engine()
302 .get_blob(BlobId::new(*id.as_bytes()))
303 .map_err(|e| map_engine_error("get_blob", &e))?;
304 let rest = blob.strip_prefix(FIELD_ROOT_DOMAIN).ok_or_else(|| {
305 Error::integrity_mismatch(format!(
306 "field manifest {id} is missing its domain prefix"
307 ))
308 })?;
309 Ok(rest.to_vec())
310 }
311 }
312 }
313}
314
315#[cfg(feature = "entropyfs-store")]
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub struct EngineStoreStats {
346 pub blob_count: u64,
348 pub logical_bytes: u64,
350 pub physical_used_bytes: u64,
352}
353
354pub struct FieldStore {
355 root: PathBuf,
356 backend: BlobBackend,
357 seeds: SeedSubstrate,
358 io: IoCounters,
359 promote: promote::PromotePolicy,
361 governor: promote::Governor,
363}
364
365impl std::fmt::Debug for FieldStore {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 f.debug_struct("FieldStore")
368 .field("root", &self.root)
369 .finish_non_exhaustive()
370 }
371}
372
373impl FieldStore {
374 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
376 let root = root.as_ref().to_path_buf();
377 crate::store::durable::create_dir_all(&root.join("descriptor"))?;
378 crate::store::durable::create_dir_all(&root.join("field"))?;
379 crate::store::durable::create_dir_all(&root.join("index"))?;
380 crate::store::durable::create_dir_all(&root.join("cache"))?;
381 let io = IoCounters::new();
382 let seeds = SeedSubstrate::Fs {
383 root: root.clone(),
384 io: io.handle(),
385 };
386 Ok(FieldStore {
387 root,
388 backend: BlobBackend::Fs,
389 seeds,
390 io,
391 promote: promote::PromotePolicy::default(),
392 governor: promote::Governor::default(),
393 })
394 }
395
396 #[cfg(feature = "entropyfs-store")]
406 pub fn open_entropyfs(root: impl AsRef<Path>) -> Result<Self> {
407 let root = root.as_ref().to_path_buf();
408 crate::store::durable::create_dir_all(&root.join("index"))?;
409 crate::store::durable::create_dir_all(&root.join("cache"))?;
410 let engine_root = root.join("entropyfs");
411 crate::store::durable::create_dir_all(&engine_root)?;
412 let fresh = fs::read_dir(&engine_root)?.next().is_none();
415 let engine = if fresh {
416 EntropyFsStore::create(&engine_root)?
417 } else {
418 EntropyFsStore::open(&engine_root)?
419 };
420 let engine = Arc::new(engine);
421 let io = IoCounters::new();
422 let seeds = SeedSubstrate::EntropyFs {
423 store: Arc::clone(&engine),
424 io: io.handle(),
425 };
426 Ok(FieldStore {
427 root,
428 backend: BlobBackend::EntropyFs(engine),
429 seeds,
430 io,
431 promote: promote::PromotePolicy::default(),
432 governor: promote::Governor::default(),
433 })
434 }
435
436 pub fn open_packed(root: impl AsRef<Path>) -> Result<Self> {
445 Self::open_packed_with_policy(root, SyncPolicy::default())
446 }
447
448 pub fn open_packed_with_policy(root: impl AsRef<Path>, policy: SyncPolicy) -> Result<Self> {
453 let root = root.as_ref().to_path_buf();
454 crate::store::durable::create_dir_all(&root.join("descriptor"))?;
455 crate::store::durable::create_dir_all(&root.join("field"))?;
456 crate::store::durable::create_dir_all(&root.join("index"))?;
457 crate::store::durable::create_dir_all(&root.join("cache"))?;
458 let io = IoCounters::new();
460 let packed = Rc::new(PackedSeedStore::open_write_with_policy(
461 &root,
462 io.handle(),
463 policy,
464 )?);
465 let seeds = SeedSubstrate::Packed { store: packed };
466 Ok(FieldStore {
467 root,
468 backend: BlobBackend::Fs,
469 seeds,
470 io,
471 promote: promote::PromotePolicy::default(),
472 governor: promote::Governor::default(),
473 })
474 }
475
476 pub fn io(&self) -> &IoCounters {
482 &self.io
483 }
484
485 pub fn root(&self) -> &Path {
487 &self.root
488 }
489
490 pub(crate) fn seed_substrate(&self) -> SeedSubstrate {
493 self.seeds.clone()
494 }
495
496 pub(crate) fn supports_partial_descriptor(&self) -> bool {
500 matches!(self.backend, BlobBackend::Fs)
501 }
502
503 pub fn seeds(&self) -> &dyn SeedStore {
505 &self.seeds
506 }
507
508 pub fn seeds_mut(&mut self) -> &mut dyn SeedStore {
510 &mut self.seeds
511 }
512
513 pub fn cache(&self) -> Result<cache::DerivedCache> {
518 cache::DerivedCache::open(self.root.join("cache"))
519 }
520
521 pub fn set_promote(&mut self, policy: promote::PromotePolicy) {
525 self.governor.set_policy(policy);
528 self.promote = policy;
529 }
530
531 pub fn promote_policy(&self) -> promote::PromotePolicy {
533 self.promote
534 }
535
536 pub(crate) fn governor(&self) -> promote::Governor {
538 self.governor.clone()
539 }
540
541 pub fn promoted(&self) -> Result<promote::PromotedStore> {
545 promote::PromotedStore::open(self.root.join("promoted"))
546 }
547
548 pub fn sync(&self) -> Result<()> {
555 match &self.backend {
556 BlobBackend::Fs => {}
557 #[cfg(feature = "entropyfs-store")]
558 BlobBackend::EntropyFs(store) => store.sync()?,
559 }
560 if let SeedSubstrate::Packed { store } = &self.seeds {
563 store.seal()?;
564 }
565 Ok(())
566 }
567
568 #[cfg(feature = "entropyfs-store")]
576 pub fn engine_stats(&self) -> Result<Option<EngineStoreStats>> {
577 match &self.backend {
578 BlobBackend::Fs => Ok(None),
579 BlobBackend::EntropyFs(store) => {
580 let m = store
581 .engine()
582 .metrics()
583 .map_err(|e| map_engine_error("metrics", &e))?;
584 Ok(Some(EngineStoreStats {
585 blob_count: m.accounting.blob_count,
586 logical_bytes: m.accounting.logical_bytes,
587 physical_used_bytes: m.accounting.physical_used_bytes,
588 }))
589 }
590 }
591 }
592
593 pub(crate) fn descriptor_path(&self, id: &Id) -> Option<PathBuf> {
596 match self.backend {
597 BlobBackend::Fs => Some(self.root.join("descriptor").join(id.to_hex())),
598 #[cfg(feature = "entropyfs-store")]
599 BlobBackend::EntropyFs(_) => None,
600 }
601 }
602
603 pub fn put_descriptor(&mut self, bytes: &[u8]) -> Result<Id> {
608 self.backend.descriptor_put(&self.root, bytes)
609 }
610
611 pub fn get_descriptor(&self, id: &Id) -> Result<Vec<u8>> {
613 let bytes = self.backend.descriptor_get(&self.root, id)?;
614 let actual = Id::of(&bytes);
615 if actual != *id {
616 return Err(Error::integrity_mismatch(format!(
617 "descriptor blob {id} hashes to {actual}"
618 )));
619 }
620 self.io.add_descriptor(bytes.len() as u64);
621 Ok(bytes)
622 }
623
624 pub fn put_field(&mut self, manifest: &FieldRoot) -> Result<FieldId> {
632 if let SeedSubstrate::Packed { store } = &self.seeds {
633 #[cfg(feature = "fault-inject")]
634 crate::fault::hit("manifest.before_flush");
635 store.flush()?;
636 #[cfg(feature = "fault-inject")]
637 crate::fault::hit("manifest.after_flush");
638 }
639 let id = self.backend.field_put(&self.root, manifest)?;
640 #[cfg(feature = "fault-inject")]
641 crate::fault::hit("manifest.after_publish");
642 Ok(id)
643 }
644
645 pub fn get_field(&self, id: &FieldId) -> Result<FieldRoot> {
647 let bytes = self.backend.field_get(&self.root, id)?;
648 let manifest = FieldRoot::decode_canonical(&bytes)?;
649 if manifest.content_id() != *id {
650 return Err(Error::integrity_mismatch(format!(
651 "field {id} manifest content id mismatch"
652 )));
653 }
654 self.io.add_manifest(bytes.len() as u64);
655 Ok(manifest)
656 }
657
658 pub fn list_fields(&self) -> Result<Vec<FieldId>> {
665 if !matches!(self.backend, BlobBackend::Fs) {
666 return Err(Error::unsupported_feature(
667 "EntropyFS exposes no per-blob enumeration; a field store backed by it \
668 cannot list its manifests (open a field by its FieldId instead)",
669 ));
670 }
671 let dir = self.root.join("field");
672 let mut out = Vec::new();
673 for entry in fs::read_dir(&dir)?.flatten() {
674 if let Some(name) = entry.file_name().to_str()
675 && let Ok(id) = FieldId::from_hex(name)
676 {
677 out.push(id);
678 }
679 }
680 out.sort_unstable();
681 Ok(out)
682 }
683
684 pub(crate) fn external_context_path(&self, id: &FieldId) -> PathBuf {
691 self.root.join("external").join(id.to_hex())
692 }
693
694 pub fn put_external_context(
701 &self,
702 id: &FieldId,
703 ctx: &external::ExternalContext,
704 ) -> Result<()> {
705 let bytes = ctx.encode_canonical();
706 write_atomic(&self.external_context_path(id), &bytes)
707 }
708
709 pub fn get_external_context(&self, id: &FieldId) -> Result<Option<external::ExternalContext>> {
712 match fs::read(self.external_context_path(id)) {
713 Ok(bytes) => external::ExternalContext::decode_canonical(&bytes).map(Some),
714 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
715 Err(e) => Err(e.into()),
716 }
717 }
718
719 pub fn clear_external_context(&self, id: &FieldId) -> Result<bool> {
723 match fs::remove_file(self.external_context_path(id)) {
724 Ok(()) => Ok(true),
725 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
726 Err(e) => Err(e.into()),
727 }
728 }
729
730 pub fn ingest(&mut self, descriptor_bytes: &[u8], limits: Limits) -> Result<FieldId> {
736 let parsed = crate::container::Descriptor::parse(descriptor_bytes, limits)?;
739 let source = crate::materialize::materialize(&parsed, limits)?;
740 self.ingest_parsed(descriptor_bytes, &parsed, &source)
741 }
742
743 pub fn ingest_verified(
754 &mut self,
755 descriptor_bytes: &[u8],
756 source: &[u8],
757 limits: Limits,
758 ) -> Result<FieldId> {
759 let mut parsed = crate::container::Descriptor::parse(descriptor_bytes, limits)?;
760 let materialized = crate::materialize::materialize_in_place(&mut parsed, limits)?;
761 if materialized.len() != source.len() || materialized != source {
762 return Err(Error::reconstruction_mismatch(format!(
763 "descriptor materialized {} bytes that differ from the supplied {} byte source",
764 materialized.len(),
765 source.len()
766 )));
767 }
768 self.ingest_parsed(descriptor_bytes, &parsed, source)
769 }
770
771 fn ingest_parsed(
776 &mut self,
777 descriptor_bytes: &[u8],
778 parsed: &ParsedDescriptor,
779 source: &[u8],
780 ) -> Result<FieldId> {
781 let descriptor_id = self.put_descriptor(descriptor_bytes)?;
782
783 let root = SeedNode::new(
789 NodeKind::DocumentExact,
790 source.len() as u64,
791 crate::integrity::sha256(source).to_vec(),
792 Vec::new(),
793 "field:document-exact",
794 );
795 let root_id = self.seeds.put_node(&root.encode_canonical())?;
796
797 let manifest = FieldRoot {
798 universe_id: crate::container::universe_id_from_str(FIELD_UNIVERSE),
799 source_sha256: parsed.descriptor.source_sha256,
800 source_len: parsed.descriptor.source_len,
801 descriptor_id: *descriptor_id.as_bytes(),
802 root_node: root_id,
803 index_root: manifest::ABSENT_ROOT,
804 node_count: 1,
805 index_node_count: 0,
806 provenance: format!("field:ingest;{}", parsed.descriptor.format_basis),
807 };
808 self.put_field(&manifest)
809 }
810}
811
812pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
819 let dir = path
820 .parent()
821 .ok_or_else(|| Error::internal_invariant("atomic write path has no parent"))?;
822 crate::store::durable::create_dir_all(dir)?;
823 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("blob");
824 let tmp = dir.join(format!(".{name}.tmp-{}", std::process::id()));
825 {
826 let mut f = crate::store::durable::create_file(&tmp)?;
827 crate::store::durable::write_all(&mut f, &tmp, bytes)?;
828 crate::store::durable::sync_all(&f, &tmp)?;
829 }
830 crate::store::durable::rename(&tmp, path)?;
831 crate::store::durable::sync_dir(dir)?;
832 Ok(())
833}
834
835pub struct Field {
837 store_root: PathBuf,
838 manifest: FieldRoot,
839 parsed: ParsedDescriptor,
840 descriptor_bytes: Vec<u8>,
841 seeds: SeedSubstrate,
845 open_io: IoSnapshot,
849}
850
851impl std::fmt::Debug for Field {
852 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
853 f.debug_struct("Field")
854 .field("manifest", &self.manifest.content_id())
855 .field("source_len", &self.manifest.source_len)
856 .finish_non_exhaustive()
857 }
858}
859
860impl Field {
861 pub fn open(store: &FieldStore, id: &FieldId, limits: Limits) -> Result<Field> {
863 let prof = warm_prof_enabled();
864 let t0 = std::time::Instant::now();
865 let io_before = store.io().snapshot();
866 let manifest = store.get_field(id)?;
867 let t_manifest = t0.elapsed();
868 let field = Field::open_after_manifest(store, manifest, io_before, limits)?;
869 if prof {
870 eprintln!(
871 "[vole-profile] manifest_read_us={} field_open_total_us={}",
872 t_manifest.as_micros(),
873 t0.elapsed().as_micros()
874 );
875 }
876 Ok(field)
877 }
878
879 pub(crate) fn open_after_manifest(
884 store: &FieldStore,
885 manifest: FieldRoot,
886 io_before: IoSnapshot,
887 limits: Limits,
888 ) -> Result<Field> {
889 let prof = warm_prof_enabled();
890 let t0 = std::time::Instant::now();
891 let descriptor_bytes = store.get_descriptor(&Id::from_bytes(manifest.descriptor_id))?;
892 let t_read = t0.elapsed();
893 let open_io = io_before.delta(&store.io().snapshot());
894 let mut parsed = crate::container::Descriptor::parse(&descriptor_bytes, limits)?;
895 if prof {
896 eprintln!(
897 "[vole-profile] descriptor_read_us={} descriptor_parse_us={} descriptor_len={}",
898 t_read.as_micros(),
899 t0.elapsed().as_micros().saturating_sub(t_read.as_micros()),
900 descriptor_bytes.len()
901 );
902 }
903 parsed.universe_id = manifest.universe_id;
904 if parsed.descriptor.source_len != manifest.source_len
906 || parsed.descriptor.source_sha256 != manifest.source_sha256
907 {
908 return Err(Error::integrity_mismatch(
909 "field manifest does not match its descriptor's declared source",
910 ));
911 }
912 Ok(Field {
913 store_root: store.root().to_path_buf(),
914 manifest,
915 parsed,
916 descriptor_bytes,
917 seeds: store.seed_substrate(),
918 open_io,
919 })
920 }
921
922 pub(crate) fn open_io(&self) -> IoSnapshot {
924 self.open_io
925 }
926
927 pub fn manifest(&self) -> &FieldRoot {
929 &self.manifest
930 }
931
932 pub fn id(&self) -> FieldId {
934 self.manifest.content_id()
935 }
936
937 pub fn parsed(&self) -> &ParsedDescriptor {
939 &self.parsed
940 }
941
942 pub fn descriptor_bytes(&self) -> &[u8] {
944 &self.descriptor_bytes
945 }
946
947 pub fn store_root(&self) -> &Path {
949 &self.store_root
950 }
951
952 pub fn materialize_exact(&self, limits: Limits) -> Result<Vec<u8>> {
958 crate::materialize::materialize(&self.parsed, limits)
959 }
960
961 pub fn materialize_node(
964 &self,
965 id: &NodeId,
966 limits: Limits,
967 budget: &mut dag::EvalBudget,
968 ) -> Result<Vec<u8>> {
969 let node = dag::load_node(&self.seeds, id)?;
970 dag::materialize_node(
971 &self.parsed,
972 &self.seeds,
973 &node,
974 limits,
975 budget,
976 node.limits.max_depth,
977 )
978 }
979}
980
981#[cfg(test)]
982mod tests {
983 use super::*;
984
985 fn temp_root(label: &str) -> PathBuf {
986 let mut p = std::env::temp_dir();
987 p.push(format!(
988 "vole-field-{label}-{}-{}",
989 std::process::id(),
990 std::time::SystemTime::now()
991 .duration_since(std::time::UNIX_EPOCH)
992 .unwrap()
993 .as_nanos()
994 ));
995 p
996 }
997
998 fn tiny_descriptor() -> Vec<u8> {
999 use crate::container::{Descriptor, ObjectSource};
1000 use crate::dra::{Op, Program};
1001 let source = b"the exact field bytes";
1002 let d = Descriptor {
1003 universe: crate::container::UNIVERSE.to_string(),
1004 source_format: crate::SOURCE_FORMAT_OPAQUE,
1005 format_basis: "opaque:field-test".to_string(),
1006 models: vec![],
1007 channels: vec![],
1008 objects: vec![ObjectSource::Inline(source.to_vec())],
1009 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
1010 observation_index: None,
1011 seek_directory: false,
1012 checkpoints: None,
1013 source_sha256: crate::integrity::sha256(source),
1014 source_len: source.len() as u64,
1015 };
1016 d.serialize().unwrap().0
1017 }
1018
1019 #[test]
1020 fn ingest_then_materialize_is_exact() {
1021 let root = temp_root("rt");
1022 let mut store = FieldStore::open(&root).unwrap();
1023 let bytes = tiny_descriptor();
1024 let id = store.ingest(&bytes, Limits::DEFAULT).unwrap();
1025 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1026 assert_eq!(
1027 field.materialize_exact(Limits::DEFAULT).unwrap(),
1028 b"the exact field bytes"
1029 );
1030 let mut budget = dag::EvalBudget::default();
1032 let out = field
1033 .materialize_node(&field.manifest.root_node, Limits::DEFAULT, &mut budget)
1034 .unwrap();
1035 assert_eq!(out, b"the exact field bytes");
1036 fs::remove_dir_all(&root).ok();
1037 }
1038
1039 #[test]
1040 fn field_id_is_stable_and_reopenable() {
1041 let root = temp_root("reopen");
1042 let bytes = tiny_descriptor();
1043 let id1 = {
1044 let mut store = FieldStore::open(&root).unwrap();
1045 store.ingest(&bytes, Limits::DEFAULT).unwrap()
1046 };
1047 let store = FieldStore::open(&root).unwrap();
1048 let m = store.get_field(&id1).unwrap();
1049 assert_eq!(m.content_id(), id1);
1050 assert_eq!(store.list_fields().unwrap(), vec![id1]);
1051 fs::remove_dir_all(&root).ok();
1052 }
1053
1054 #[test]
1055 fn promotion_is_a_noop_by_default() {
1056 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1057 let root = temp_root("promote-off");
1058 let mut store = FieldStore::open(&root).unwrap();
1059 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
1060 let req = ObserveRequest::new(
1061 Selector::ByteRange { offset: 4, len: 5 },
1062 Representation::ExactBytes,
1063 );
1064 let (a1, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1065 let (a2, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1066 assert_eq!(a1, a2);
1067 assert!(!root.join("promoted").exists(), "promotion must be opt-in");
1069 fs::remove_dir_all(&root).ok();
1070 }
1071
1072 #[test]
1073 fn promotion_preserves_the_answer_and_the_durable_store_serves_it() {
1074 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1075 use crate::field::promote::PromotePolicy;
1076 let root = temp_root("promote-on");
1077 let mut store = FieldStore::open(&root).unwrap();
1078 store.set_promote(PromotePolicy {
1080 enabled: true,
1081 min_hits: 1,
1082 rent_per_byte: 0,
1083 ..PromotePolicy::default()
1084 });
1085 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
1086 let req = ObserveRequest::new(
1087 Selector::ByteRange { offset: 4, len: 5 },
1088 Representation::ExactBytes,
1089 );
1090 let (cold, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1091 let (warm, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1092 assert_eq!(cold, warm, "promotion must not change the answer");
1093 let promoted = store.promoted().unwrap();
1095 let ob = promoted.output_bytes().unwrap();
1096 assert!(
1097 ob > 0,
1098 "the reused node must be in the durable store (output_bytes={ob}, promoted_dir_exists={})",
1099 root.join("promoted").exists()
1100 );
1101 crate::field::cache::DerivedCache::open(root.join("cache"))
1103 .unwrap()
1104 .clear()
1105 .unwrap();
1106 let (after, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1107 assert_eq!(cold, after, "the promoted store must yield the same answer");
1108 fs::remove_dir_all(&root).ok();
1109 }
1110}
1111
1112#[cfg(all(test, feature = "entropyfs-store"))]
1115mod entropyfs_field_tests {
1116 use super::*;
1117 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1118 use crate::field::provenance::AnswerValue;
1119 use crate::store::seed_closure;
1120
1121 fn temp_root(label: &str) -> PathBuf {
1122 let mut p = std::env::temp_dir();
1123 p.push(format!(
1124 "vole-field-entropyfs-{label}-{}-{}",
1125 std::process::id(),
1126 std::time::SystemTime::now()
1127 .duration_since(std::time::UNIX_EPOCH)
1128 .unwrap()
1129 .as_nanos()
1130 ));
1131 p
1132 }
1133
1134 fn tiny_descriptor() -> Vec<u8> {
1135 use crate::container::{Descriptor, ObjectSource};
1136 use crate::dra::{Op, Program};
1137 let source = b"the exact field bytes";
1138 let d = Descriptor {
1139 universe: crate::container::UNIVERSE.to_string(),
1140 source_format: crate::SOURCE_FORMAT_OPAQUE,
1141 format_basis: "opaque:field-entropyfs-test".to_string(),
1142 models: vec![],
1143 channels: vec![],
1144 objects: vec![ObjectSource::Inline(source.to_vec())],
1145 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
1146 observation_index: None,
1147 seek_directory: false,
1148 checkpoints: None,
1149 source_sha256: crate::integrity::sha256(source),
1150 source_len: source.len() as u64,
1151 };
1152 d.serialize().unwrap().0
1153 }
1154
1155 #[test]
1156 fn ingest_observe_and_exact_materialize_through_the_engine() {
1157 let root = temp_root("rt");
1158 let descriptor = tiny_descriptor();
1159 let expected = b"the exact field bytes";
1160 let mut store = FieldStore::open_entropyfs(&root).unwrap();
1161 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1162 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1163 assert_eq!(field.materialize_exact(Limits::DEFAULT).unwrap(), expected);
1164 let mut budget = dag::EvalBudget::default();
1166 assert_eq!(
1167 field
1168 .materialize_node(&field.manifest().root_node, Limits::DEFAULT, &mut budget)
1169 .unwrap(),
1170 expected
1171 );
1172 let req = ObserveRequest::new(
1174 Selector::ByteRange { offset: 4, len: 5 },
1175 Representation::ExactBytes,
1176 );
1177 let (answer, _stats, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1178 match answer.value {
1179 AnswerValue::Bytes(b) => assert_eq!(b, b"exact"),
1180 other => panic!("expected bytes, got {other:?}"),
1181 }
1182 assert_eq!(
1185 store.list_fields().unwrap_err().class(),
1186 crate::ErrorClass::UnsupportedFeature
1187 );
1188 std::fs::remove_dir_all(&root).ok();
1189 }
1190
1191 #[test]
1192 fn cross_process_reopen_reads_what_a_previous_handle_wrote() {
1193 let root = temp_root("reopen");
1194 let descriptor = tiny_descriptor();
1195 let (id, root_node) = {
1198 let mut store = FieldStore::open_entropyfs(&root).unwrap();
1199 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1200 let node = store.get_field(&id).unwrap().root_node;
1201 store.sync().unwrap();
1203 (id, node)
1204 };
1205 let store = FieldStore::open_entropyfs(&root).unwrap();
1207 let manifest = store.get_field(&id).unwrap();
1208 assert_eq!(manifest.content_id(), id);
1209 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1210 assert_eq!(
1211 field.materialize_exact(Limits::DEFAULT).unwrap(),
1212 b"the exact field bytes"
1213 );
1214 let mut budget = dag::EvalBudget::default();
1215 assert_eq!(
1216 field
1217 .materialize_node(&root_node, Limits::DEFAULT, &mut budget)
1218 .unwrap(),
1219 b"the exact field bytes"
1220 );
1221 std::fs::remove_dir_all(&root).ok();
1222 }
1223
1224 #[test]
1225 fn list_nodes_declines_so_gc_cannot_sweep() {
1226 let root = temp_root("gc");
1227 let mut store = FieldStore::open_entropyfs(&root).unwrap();
1228 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
1229 let manifest = store.get_field(&id).unwrap();
1230 let e = store.seeds().list_nodes().unwrap_err();
1232 assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
1233 let reachable =
1236 seed_closure(store.seeds(), &[manifest.root_node], |_| Ok(Vec::new())).unwrap();
1237 assert!(reachable.contains(&manifest.root_node));
1238 std::fs::remove_dir_all(&root).ok();
1239 }
1240}
1241
1242#[cfg(test)]
1246mod packed_field_tests {
1247 use super::*;
1248 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1249 use crate::field::provenance::AnswerValue;
1250
1251 fn temp_root(label: &str) -> PathBuf {
1252 let mut p = std::env::temp_dir();
1253 p.push(format!(
1254 "vole-field-packed-{label}-{}-{}",
1255 std::process::id(),
1256 std::time::SystemTime::now()
1257 .duration_since(std::time::UNIX_EPOCH)
1258 .unwrap()
1259 .as_nanos()
1260 ));
1261 p
1262 }
1263
1264 fn tiny_descriptor() -> Vec<u8> {
1265 use crate::container::{Descriptor, ObjectSource};
1266 use crate::dra::{Op, Program};
1267 let source = b"the exact field bytes";
1268 let d = Descriptor {
1269 universe: crate::container::UNIVERSE.to_string(),
1270 source_format: crate::SOURCE_FORMAT_OPAQUE,
1271 format_basis: "opaque:field-packed-test".to_string(),
1272 models: vec![],
1273 channels: vec![],
1274 objects: vec![ObjectSource::Inline(source.to_vec())],
1275 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
1276 observation_index: None,
1277 seek_directory: false,
1278 checkpoints: None,
1279 source_sha256: crate::integrity::sha256(source),
1280 source_len: source.len() as u64,
1281 };
1282 d.serialize().unwrap().0
1283 }
1284
1285 #[test]
1286 fn packed_matches_fs_for_ingest_observe_and_exact_materialize() {
1287 let fs_root = temp_root("fs");
1288 let pk_root = temp_root("packed");
1289 let descriptor = tiny_descriptor();
1290 let expected = b"the exact field bytes";
1291
1292 let mut fs_store = FieldStore::open(&fs_root).unwrap();
1293 let fs_id = fs_store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1294 fs_store.sync().unwrap();
1295
1296 let mut pk_store = FieldStore::open_packed(&pk_root).unwrap();
1297 let pk_id = pk_store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1298 pk_store.sync().unwrap();
1299
1300 assert_eq!(
1303 fs_id, pk_id,
1304 "the packed backend must not change the FieldId"
1305 );
1306 assert_eq!(
1307 fs_store.get_field(&fs_id).unwrap().root_node,
1308 pk_store.get_field(&pk_id).unwrap().root_node,
1309 "node identity is content-derived and backend-independent"
1310 );
1311
1312 {
1314 let f = Field::open(&fs_store, &fs_id, Limits::DEFAULT).unwrap();
1315 assert_eq!(f.materialize_exact(Limits::DEFAULT).unwrap(), expected);
1316 let p = Field::open(&pk_store, &pk_id, Limits::DEFAULT).unwrap();
1317 assert_eq!(p.materialize_exact(Limits::DEFAULT).unwrap(), expected);
1318 }
1319
1320 let req = ObserveRequest::new(
1322 Selector::ByteRange { offset: 4, len: 5 },
1323 Representation::ExactBytes,
1324 );
1325 let (fs_ans, _, _) =
1326 observe::observe(&mut fs_store, &fs_id, &req, Limits::DEFAULT).unwrap();
1327 let (pk_ans, _, _) =
1328 observe::observe(&mut pk_store, &pk_id, &req, Limits::DEFAULT).unwrap();
1329 match (&fs_ans.value, &pk_ans.value) {
1330 (AnswerValue::Bytes(a), AnswerValue::Bytes(b)) => {
1331 assert_eq!(a, b, "packed and fs observations must agree");
1332 assert_eq!(b, b"exact");
1333 }
1334 other => panic!("expected byte answers, got {other:?}"),
1335 }
1336
1337 std::fs::remove_dir_all(&fs_root).ok();
1338 std::fs::remove_dir_all(&pk_root).ok();
1339 }
1340
1341 #[test]
1342 fn packed_reopen_serves_the_same_field() {
1343 let root = temp_root("reopen");
1344 let descriptor = tiny_descriptor();
1345 let id = {
1346 let mut store = FieldStore::open_packed(&root).unwrap();
1347 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1348 store.sync().unwrap();
1350 id
1351 };
1352 let store = FieldStore::open_packed(&root).unwrap();
1353 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1354 assert_eq!(
1355 field.materialize_exact(Limits::DEFAULT).unwrap(),
1356 b"the exact field bytes"
1357 );
1358 std::fs::remove_dir_all(&root).ok();
1359 }
1360}