1pub mod build;
16pub mod cache;
17pub mod capabilities;
18pub mod dag;
19pub mod derive;
20pub mod document_format;
21pub mod edit;
22pub mod explain;
23pub mod external;
24pub mod index;
25pub(crate) mod inflate;
28pub mod ingest;
29#[cfg(feature = "package")]
30pub mod ingest_package;
31pub mod manifest;
32pub mod node;
33pub mod observe;
34#[cfg(feature = "opc")]
35pub mod opc;
36pub mod partial;
37pub mod plan;
38pub(crate) mod prof;
39pub mod promote;
40pub mod provenance;
41pub mod resource;
42pub mod session;
43pub mod share;
44
45pub use manifest::{FieldId, FieldRoot};
46
47pub(crate) fn warm_prof_enabled() -> bool {
51 std::env::var_os("VOLE_PROFILE_OPEN").is_some()
52}
53
54use std::fs;
55use std::path::{Path, PathBuf};
56use std::rc::Rc;
57
58#[cfg(feature = "entropyfs-store")]
59use std::sync::Arc;
60
61#[cfg(feature = "entropyfs-store")]
62use entropyfs::engine::BlobId;
63
64use crate::container::ParsedDescriptor;
65use crate::error::{Error, Result};
66use crate::limits::Limits;
67#[cfg(feature = "entropyfs-store")]
68use crate::store::{EntropyFsStore, map_engine_error};
69use crate::store::{
70 FsSeedStore, Id, IoCounters, IoSnapshot, NodeId, PackedSeedStore, SeedStore, SyncPolicy,
71};
72
73#[cfg(feature = "entropyfs-store")]
74use self::manifest::FIELD_ROOT_DOMAIN;
75use self::node::{NodeKind, SeedNode};
76
77pub 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";
79
80#[cfg(feature = "package")]
87pub 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";
88
89#[derive(Clone)]
98pub(crate) enum SeedSubstrate {
99 Fs { root: PathBuf, io: IoCounters },
101 Packed { store: Rc<PackedSeedStore> },
103 #[cfg(feature = "entropyfs-store")]
105 EntropyFs {
106 store: Arc<EntropyFsStore>,
107 io: IoCounters,
108 },
109}
110
111impl SeedStore for SeedSubstrate {
112 fn put_node(&mut self, canonical: &[u8]) -> Result<NodeId> {
113 match self {
114 SeedSubstrate::Fs { root, io } => {
115 FsSeedStore::open_with_io(root, io.handle())?.put_node(canonical)
116 }
117 SeedSubstrate::Packed { store } => store.insert(canonical),
118 #[cfg(feature = "entropyfs-store")]
119 SeedSubstrate::EntropyFs { store, .. } => store.seed_put(canonical),
120 }
121 }
122
123 fn get_node(&self, id: &NodeId) -> Result<Vec<u8>> {
124 match self {
125 SeedSubstrate::Fs { root, io } => {
126 FsSeedStore::open_with_io(root, io.handle())?.get_node(id)
127 }
128 SeedSubstrate::Packed { store } => store.fetch(id),
129 #[cfg(feature = "entropyfs-store")]
130 SeedSubstrate::EntropyFs { store, io } => {
131 let bytes = store.seed_get(id)?;
132 io.add_seed(bytes.len() as u64);
133 Ok(bytes)
134 }
135 }
136 }
137
138 fn get_node_range(&self, id: &NodeId, offset: u64, len: u64) -> Result<Vec<u8>> {
139 match self {
140 SeedSubstrate::Fs { root, io } => {
141 FsSeedStore::open_with_io(root, io.handle())?.get_node_range(id, offset, len)
142 }
143 SeedSubstrate::Packed { store } => store.fetch_range(id, offset, len),
144 #[cfg(feature = "entropyfs-store")]
145 SeedSubstrate::EntropyFs { store, io } => {
146 let bytes = store.seed_get_range(id, offset, len)?;
147 io.add_seed(bytes.len() as u64);
148 Ok(bytes)
149 }
150 }
151 }
152
153 fn contains_node(&self, id: &NodeId) -> Result<bool> {
154 match self {
155 SeedSubstrate::Fs { root, io } => {
156 FsSeedStore::open_with_io(root, io.handle())?.contains_node(id)
157 }
158 SeedSubstrate::Packed { store } => store.has(id),
159 #[cfg(feature = "entropyfs-store")]
160 SeedSubstrate::EntropyFs { store, .. } => store.seed_contains(id),
161 }
162 }
163
164 fn list_nodes(&self) -> Result<Vec<(NodeId, u64)>> {
165 match self {
166 SeedSubstrate::Fs { root, io } => {
167 FsSeedStore::open_with_io(root, io.handle())?.list_nodes()
168 }
169 SeedSubstrate::Packed { store } => store.entries(),
170 #[cfg(feature = "entropyfs-store")]
171 SeedSubstrate::EntropyFs { store, .. } => store.seed_list(),
172 }
173 }
174}
175
176enum BlobBackend {
184 Fs,
186 #[cfg(feature = "entropyfs-store")]
192 EntropyFs(Arc<EntropyFsStore>),
193}
194
195impl BlobBackend {
196 fn descriptor_put(&self, root: &Path, bytes: &[u8]) -> Result<Id> {
197 match self {
198 BlobBackend::Fs => {
199 let id = Id::of(bytes);
200 let path = root.join("descriptor").join(id.to_hex());
201 if !path.exists() {
202 write_atomic(&path, bytes)?;
203 }
204 Ok(id)
205 }
206 #[cfg(feature = "entropyfs-store")]
207 BlobBackend::EntropyFs(store) => {
208 let id = Id::of(bytes);
209 let blob = store
210 .engine()
211 .put_blob(bytes)
212 .map_err(|e| map_engine_error("put_blob", &e))?;
213 debug_assert_eq!(blob.as_bytes(), id.as_bytes());
214 Ok(id)
215 }
216 }
217 }
218
219 fn descriptor_get(&self, root: &Path, id: &Id) -> Result<Vec<u8>> {
220 match self {
221 BlobBackend::Fs => {
222 let path = root.join("descriptor").join(id.to_hex());
223 fs::read(&path).map_err(|e| {
224 if e.kind() == std::io::ErrorKind::NotFound {
225 Error::missing_external_object(format!(
226 "descriptor blob {id} is not present"
227 ))
228 } else {
229 Error::io(format!("reading descriptor blob {id}: {e}"))
230 }
231 })
232 }
233 #[cfg(feature = "entropyfs-store")]
234 BlobBackend::EntropyFs(store) => store
235 .engine()
236 .get_blob(BlobId::new(*id.as_bytes()))
237 .map_err(|e| map_engine_error("get_blob", &e)),
238 }
239 }
240
241 fn field_put(&self, root: &Path, manifest: &FieldRoot) -> Result<FieldId> {
242 let bytes = manifest.encode_canonical();
243 let id = FieldId::of_manifest(&bytes);
244 match self {
245 BlobBackend::Fs => {
246 let path = root.join("field").join(id.to_hex());
247 if !path.exists() {
248 write_atomic(&path, &bytes)?;
249 }
250 Ok(id)
251 }
252 #[cfg(feature = "entropyfs-store")]
253 BlobBackend::EntropyFs(store) => {
254 let mut prefixed = Vec::with_capacity(FIELD_ROOT_DOMAIN.len() + bytes.len());
255 prefixed.extend_from_slice(FIELD_ROOT_DOMAIN);
256 prefixed.extend_from_slice(&bytes);
257 let blob = store
258 .engine()
259 .put_blob(&prefixed)
260 .map_err(|e| map_engine_error("put_blob", &e))?;
261 debug_assert_eq!(blob.as_bytes(), id.as_bytes());
262 Ok(id)
263 }
264 }
265 }
266
267 fn field_get(&self, root: &Path, id: &FieldId) -> Result<Vec<u8>> {
268 match self {
269 BlobBackend::Fs => {
270 let path = root.join("field").join(id.to_hex());
271 fs::read(&path).map_err(|e| {
272 if e.kind() == std::io::ErrorKind::NotFound {
273 Error::missing_external_object(format!("field {id} is not present"))
274 } else {
275 Error::io(format!("reading field {id}: {e}"))
276 }
277 })
278 }
279 #[cfg(feature = "entropyfs-store")]
280 BlobBackend::EntropyFs(store) => {
281 let blob = store
282 .engine()
283 .get_blob(BlobId::new(*id.as_bytes()))
284 .map_err(|e| map_engine_error("get_blob", &e))?;
285 let rest = blob.strip_prefix(FIELD_ROOT_DOMAIN).ok_or_else(|| {
286 Error::integrity_mismatch(format!(
287 "field manifest {id} is missing its domain prefix"
288 ))
289 })?;
290 Ok(rest.to_vec())
291 }
292 }
293 }
294}
295
296#[cfg(feature = "entropyfs-store")]
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub struct EngineStoreStats {
327 pub blob_count: u64,
329 pub logical_bytes: u64,
331 pub physical_used_bytes: u64,
333}
334
335pub struct FieldStore {
336 root: PathBuf,
337 backend: BlobBackend,
338 seeds: SeedSubstrate,
339 io: IoCounters,
340 promote: promote::PromotePolicy,
342 governor: promote::Governor,
344}
345
346impl std::fmt::Debug for FieldStore {
347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 f.debug_struct("FieldStore")
349 .field("root", &self.root)
350 .finish_non_exhaustive()
351 }
352}
353
354impl FieldStore {
355 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
357 let root = root.as_ref().to_path_buf();
358 crate::store::durable::create_dir_all(&root.join("descriptor"))?;
359 crate::store::durable::create_dir_all(&root.join("field"))?;
360 crate::store::durable::create_dir_all(&root.join("index"))?;
361 crate::store::durable::create_dir_all(&root.join("cache"))?;
362 let io = IoCounters::new();
363 let seeds = SeedSubstrate::Fs {
364 root: root.clone(),
365 io: io.handle(),
366 };
367 Ok(FieldStore {
368 root,
369 backend: BlobBackend::Fs,
370 seeds,
371 io,
372 promote: promote::PromotePolicy::default(),
373 governor: promote::Governor::default(),
374 })
375 }
376
377 #[cfg(feature = "entropyfs-store")]
387 pub fn open_entropyfs(root: impl AsRef<Path>) -> Result<Self> {
388 let root = root.as_ref().to_path_buf();
389 crate::store::durable::create_dir_all(&root.join("index"))?;
390 crate::store::durable::create_dir_all(&root.join("cache"))?;
391 let engine_root = root.join("entropyfs");
392 crate::store::durable::create_dir_all(&engine_root)?;
393 let fresh = fs::read_dir(&engine_root)?.next().is_none();
396 let engine = if fresh {
397 EntropyFsStore::create(&engine_root)?
398 } else {
399 EntropyFsStore::open(&engine_root)?
400 };
401 let engine = Arc::new(engine);
402 let io = IoCounters::new();
403 let seeds = SeedSubstrate::EntropyFs {
404 store: Arc::clone(&engine),
405 io: io.handle(),
406 };
407 Ok(FieldStore {
408 root,
409 backend: BlobBackend::EntropyFs(engine),
410 seeds,
411 io,
412 promote: promote::PromotePolicy::default(),
413 governor: promote::Governor::default(),
414 })
415 }
416
417 pub fn open_packed(root: impl AsRef<Path>) -> Result<Self> {
426 Self::open_packed_with_policy(root, SyncPolicy::default())
427 }
428
429 pub fn open_packed_with_policy(root: impl AsRef<Path>, policy: SyncPolicy) -> Result<Self> {
434 let root = root.as_ref().to_path_buf();
435 crate::store::durable::create_dir_all(&root.join("descriptor"))?;
436 crate::store::durable::create_dir_all(&root.join("field"))?;
437 crate::store::durable::create_dir_all(&root.join("index"))?;
438 crate::store::durable::create_dir_all(&root.join("cache"))?;
439 let io = IoCounters::new();
441 let packed = Rc::new(PackedSeedStore::open_write_with_policy(
442 &root,
443 io.handle(),
444 policy,
445 )?);
446 let seeds = SeedSubstrate::Packed { store: packed };
447 Ok(FieldStore {
448 root,
449 backend: BlobBackend::Fs,
450 seeds,
451 io,
452 promote: promote::PromotePolicy::default(),
453 governor: promote::Governor::default(),
454 })
455 }
456
457 pub fn io(&self) -> &IoCounters {
463 &self.io
464 }
465
466 pub fn root(&self) -> &Path {
468 &self.root
469 }
470
471 pub(crate) fn seed_substrate(&self) -> SeedSubstrate {
474 self.seeds.clone()
475 }
476
477 pub(crate) fn supports_partial_descriptor(&self) -> bool {
481 matches!(self.backend, BlobBackend::Fs)
482 }
483
484 pub fn seeds(&self) -> &dyn SeedStore {
486 &self.seeds
487 }
488
489 pub fn seeds_mut(&mut self) -> &mut dyn SeedStore {
491 &mut self.seeds
492 }
493
494 pub fn cache(&self) -> Result<cache::DerivedCache> {
499 cache::DerivedCache::open(self.root.join("cache"))
500 }
501
502 pub fn set_promote(&mut self, policy: promote::PromotePolicy) {
506 self.governor.set_policy(policy);
509 self.promote = policy;
510 }
511
512 pub fn promote_policy(&self) -> promote::PromotePolicy {
514 self.promote
515 }
516
517 pub(crate) fn governor(&self) -> promote::Governor {
519 self.governor.clone()
520 }
521
522 pub fn promoted(&self) -> Result<promote::PromotedStore> {
526 promote::PromotedStore::open(self.root.join("promoted"))
527 }
528
529 pub fn sync(&self) -> Result<()> {
536 match &self.backend {
537 BlobBackend::Fs => {}
538 #[cfg(feature = "entropyfs-store")]
539 BlobBackend::EntropyFs(store) => store.sync()?,
540 }
541 if let SeedSubstrate::Packed { store } = &self.seeds {
544 store.seal()?;
545 }
546 Ok(())
547 }
548
549 #[cfg(feature = "entropyfs-store")]
557 pub fn engine_stats(&self) -> Result<Option<EngineStoreStats>> {
558 match &self.backend {
559 BlobBackend::Fs => Ok(None),
560 BlobBackend::EntropyFs(store) => {
561 let m = store
562 .engine()
563 .metrics()
564 .map_err(|e| map_engine_error("metrics", &e))?;
565 Ok(Some(EngineStoreStats {
566 blob_count: m.accounting.blob_count,
567 logical_bytes: m.accounting.logical_bytes,
568 physical_used_bytes: m.accounting.physical_used_bytes,
569 }))
570 }
571 }
572 }
573
574 pub(crate) fn descriptor_path(&self, id: &Id) -> Option<PathBuf> {
577 match self.backend {
578 BlobBackend::Fs => Some(self.root.join("descriptor").join(id.to_hex())),
579 #[cfg(feature = "entropyfs-store")]
580 BlobBackend::EntropyFs(_) => None,
581 }
582 }
583
584 pub fn put_descriptor(&mut self, bytes: &[u8]) -> Result<Id> {
589 self.backend.descriptor_put(&self.root, bytes)
590 }
591
592 pub fn get_descriptor(&self, id: &Id) -> Result<Vec<u8>> {
594 let bytes = self.backend.descriptor_get(&self.root, id)?;
595 let actual = Id::of(&bytes);
596 if actual != *id {
597 return Err(Error::integrity_mismatch(format!(
598 "descriptor blob {id} hashes to {actual}"
599 )));
600 }
601 self.io.add_descriptor(bytes.len() as u64);
602 Ok(bytes)
603 }
604
605 pub fn put_field(&mut self, manifest: &FieldRoot) -> Result<FieldId> {
613 if let SeedSubstrate::Packed { store } = &self.seeds {
614 #[cfg(feature = "fault-inject")]
615 crate::fault::hit("manifest.before_flush");
616 store.flush()?;
617 #[cfg(feature = "fault-inject")]
618 crate::fault::hit("manifest.after_flush");
619 }
620 let id = self.backend.field_put(&self.root, manifest)?;
621 #[cfg(feature = "fault-inject")]
622 crate::fault::hit("manifest.after_publish");
623 Ok(id)
624 }
625
626 pub fn get_field(&self, id: &FieldId) -> Result<FieldRoot> {
628 let bytes = self.backend.field_get(&self.root, id)?;
629 let manifest = FieldRoot::decode_canonical(&bytes)?;
630 if manifest.content_id() != *id {
631 return Err(Error::integrity_mismatch(format!(
632 "field {id} manifest content id mismatch"
633 )));
634 }
635 self.io.add_manifest(bytes.len() as u64);
636 Ok(manifest)
637 }
638
639 pub fn list_fields(&self) -> Result<Vec<FieldId>> {
646 if !matches!(self.backend, BlobBackend::Fs) {
647 return Err(Error::unsupported_feature(
648 "EntropyFS exposes no per-blob enumeration; a field store backed by it \
649 cannot list its manifests (open a field by its FieldId instead)",
650 ));
651 }
652 let dir = self.root.join("field");
653 let mut out = Vec::new();
654 for entry in fs::read_dir(&dir)?.flatten() {
655 if let Some(name) = entry.file_name().to_str()
656 && let Ok(id) = FieldId::from_hex(name)
657 {
658 out.push(id);
659 }
660 }
661 out.sort_unstable();
662 Ok(out)
663 }
664
665 pub(crate) fn external_context_path(&self, id: &FieldId) -> PathBuf {
672 self.root.join("external").join(id.to_hex())
673 }
674
675 pub fn put_external_context(
682 &self,
683 id: &FieldId,
684 ctx: &external::ExternalContext,
685 ) -> Result<()> {
686 let bytes = ctx.encode_canonical();
687 write_atomic(&self.external_context_path(id), &bytes)
688 }
689
690 pub fn get_external_context(&self, id: &FieldId) -> Result<Option<external::ExternalContext>> {
693 match fs::read(self.external_context_path(id)) {
694 Ok(bytes) => external::ExternalContext::decode_canonical(&bytes).map(Some),
695 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
696 Err(e) => Err(e.into()),
697 }
698 }
699
700 pub fn clear_external_context(&self, id: &FieldId) -> Result<bool> {
704 match fs::remove_file(self.external_context_path(id)) {
705 Ok(()) => Ok(true),
706 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
707 Err(e) => Err(e.into()),
708 }
709 }
710
711 pub fn ingest(&mut self, descriptor_bytes: &[u8], limits: Limits) -> Result<FieldId> {
717 let parsed = crate::container::Descriptor::parse(descriptor_bytes, limits)?;
720 let source = crate::materialize::materialize(&parsed, limits)?;
721 self.ingest_parsed(descriptor_bytes, &parsed, &source)
722 }
723
724 pub fn ingest_verified(
735 &mut self,
736 descriptor_bytes: &[u8],
737 source: &[u8],
738 limits: Limits,
739 ) -> Result<FieldId> {
740 let mut parsed = crate::container::Descriptor::parse(descriptor_bytes, limits)?;
741 let materialized = crate::materialize::materialize_in_place(&mut parsed, limits)?;
742 if materialized.len() != source.len() || materialized != source {
743 return Err(Error::reconstruction_mismatch(format!(
744 "descriptor materialized {} bytes that differ from the supplied {} byte source",
745 materialized.len(),
746 source.len()
747 )));
748 }
749 self.ingest_parsed(descriptor_bytes, &parsed, source)
750 }
751
752 fn ingest_parsed(
757 &mut self,
758 descriptor_bytes: &[u8],
759 parsed: &ParsedDescriptor,
760 source: &[u8],
761 ) -> Result<FieldId> {
762 let descriptor_id = self.put_descriptor(descriptor_bytes)?;
763
764 let root = SeedNode::new(
770 NodeKind::DocumentExact,
771 source.len() as u64,
772 crate::integrity::sha256(source).to_vec(),
773 Vec::new(),
774 "field:document-exact",
775 );
776 let root_id = self.seeds.put_node(&root.encode_canonical())?;
777
778 let manifest = FieldRoot {
779 universe_id: crate::container::universe_id_from_str(FIELD_UNIVERSE),
780 source_sha256: parsed.descriptor.source_sha256,
781 source_len: parsed.descriptor.source_len,
782 descriptor_id: *descriptor_id.as_bytes(),
783 root_node: root_id,
784 index_root: manifest::ABSENT_ROOT,
785 node_count: 1,
786 index_node_count: 0,
787 provenance: format!("field:ingest;{}", parsed.descriptor.format_basis),
788 };
789 self.put_field(&manifest)
790 }
791}
792
793pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
800 let dir = path
801 .parent()
802 .ok_or_else(|| Error::internal_invariant("atomic write path has no parent"))?;
803 crate::store::durable::create_dir_all(dir)?;
804 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("blob");
805 let tmp = dir.join(format!(".{name}.tmp-{}", std::process::id()));
806 {
807 let mut f = crate::store::durable::create_file(&tmp)?;
808 crate::store::durable::write_all(&mut f, &tmp, bytes)?;
809 crate::store::durable::sync_all(&f, &tmp)?;
810 }
811 crate::store::durable::rename(&tmp, path)?;
812 crate::store::durable::sync_dir(dir)?;
813 Ok(())
814}
815
816pub struct Field {
818 store_root: PathBuf,
819 manifest: FieldRoot,
820 parsed: ParsedDescriptor,
821 descriptor_bytes: Vec<u8>,
822 seeds: SeedSubstrate,
826 open_io: IoSnapshot,
830}
831
832impl std::fmt::Debug for Field {
833 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
834 f.debug_struct("Field")
835 .field("manifest", &self.manifest.content_id())
836 .field("source_len", &self.manifest.source_len)
837 .finish_non_exhaustive()
838 }
839}
840
841impl Field {
842 pub fn open(store: &FieldStore, id: &FieldId, limits: Limits) -> Result<Field> {
844 let prof = warm_prof_enabled();
845 let t0 = std::time::Instant::now();
846 let io_before = store.io().snapshot();
847 let manifest = store.get_field(id)?;
848 let t_manifest = t0.elapsed();
849 let field = Field::open_after_manifest(store, manifest, io_before, limits)?;
850 if prof {
851 eprintln!(
852 "[vole-profile] manifest_read_us={} field_open_total_us={}",
853 t_manifest.as_micros(),
854 t0.elapsed().as_micros()
855 );
856 }
857 Ok(field)
858 }
859
860 pub(crate) fn open_after_manifest(
865 store: &FieldStore,
866 manifest: FieldRoot,
867 io_before: IoSnapshot,
868 limits: Limits,
869 ) -> Result<Field> {
870 let prof = warm_prof_enabled();
871 let t0 = std::time::Instant::now();
872 let descriptor_bytes = store.get_descriptor(&Id::from_bytes(manifest.descriptor_id))?;
873 let t_read = t0.elapsed();
874 let open_io = io_before.delta(&store.io().snapshot());
875 let mut parsed = crate::container::Descriptor::parse(&descriptor_bytes, limits)?;
876 if prof {
877 eprintln!(
878 "[vole-profile] descriptor_read_us={} descriptor_parse_us={} descriptor_len={}",
879 t_read.as_micros(),
880 t0.elapsed().as_micros().saturating_sub(t_read.as_micros()),
881 descriptor_bytes.len()
882 );
883 }
884 parsed.universe_id = manifest.universe_id;
885 if parsed.descriptor.source_len != manifest.source_len
887 || parsed.descriptor.source_sha256 != manifest.source_sha256
888 {
889 return Err(Error::integrity_mismatch(
890 "field manifest does not match its descriptor's declared source",
891 ));
892 }
893 Ok(Field {
894 store_root: store.root().to_path_buf(),
895 manifest,
896 parsed,
897 descriptor_bytes,
898 seeds: store.seed_substrate(),
899 open_io,
900 })
901 }
902
903 pub(crate) fn open_io(&self) -> IoSnapshot {
905 self.open_io
906 }
907
908 pub fn manifest(&self) -> &FieldRoot {
910 &self.manifest
911 }
912
913 pub fn id(&self) -> FieldId {
915 self.manifest.content_id()
916 }
917
918 pub fn parsed(&self) -> &ParsedDescriptor {
920 &self.parsed
921 }
922
923 pub fn descriptor_bytes(&self) -> &[u8] {
925 &self.descriptor_bytes
926 }
927
928 pub fn store_root(&self) -> &Path {
930 &self.store_root
931 }
932
933 pub fn materialize_exact(&self, limits: Limits) -> Result<Vec<u8>> {
939 crate::materialize::materialize(&self.parsed, limits)
940 }
941
942 pub fn materialize_node(
945 &self,
946 id: &NodeId,
947 limits: Limits,
948 budget: &mut dag::EvalBudget,
949 ) -> Result<Vec<u8>> {
950 let node = dag::load_node(&self.seeds, id)?;
951 dag::materialize_node(
952 &self.parsed,
953 &self.seeds,
954 &node,
955 limits,
956 budget,
957 node.limits.max_depth,
958 )
959 }
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965
966 fn temp_root(label: &str) -> PathBuf {
967 let mut p = std::env::temp_dir();
968 p.push(format!(
969 "vole-field-{label}-{}-{}",
970 std::process::id(),
971 std::time::SystemTime::now()
972 .duration_since(std::time::UNIX_EPOCH)
973 .unwrap()
974 .as_nanos()
975 ));
976 p
977 }
978
979 fn tiny_descriptor() -> Vec<u8> {
980 use crate::container::{Descriptor, ObjectSource};
981 use crate::dra::{Op, Program};
982 let source = b"the exact field bytes";
983 let d = Descriptor {
984 universe: crate::container::UNIVERSE.to_string(),
985 source_format: crate::SOURCE_FORMAT_OPAQUE,
986 format_basis: "opaque:field-test".to_string(),
987 models: vec![],
988 channels: vec![],
989 objects: vec![ObjectSource::Inline(source.to_vec())],
990 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
991 observation_index: None,
992 seek_directory: false,
993 checkpoints: None,
994 source_sha256: crate::integrity::sha256(source),
995 source_len: source.len() as u64,
996 };
997 d.serialize().unwrap().0
998 }
999
1000 #[test]
1001 fn ingest_then_materialize_is_exact() {
1002 let root = temp_root("rt");
1003 let mut store = FieldStore::open(&root).unwrap();
1004 let bytes = tiny_descriptor();
1005 let id = store.ingest(&bytes, Limits::DEFAULT).unwrap();
1006 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1007 assert_eq!(
1008 field.materialize_exact(Limits::DEFAULT).unwrap(),
1009 b"the exact field bytes"
1010 );
1011 let mut budget = dag::EvalBudget::default();
1013 let out = field
1014 .materialize_node(&field.manifest.root_node, Limits::DEFAULT, &mut budget)
1015 .unwrap();
1016 assert_eq!(out, b"the exact field bytes");
1017 fs::remove_dir_all(&root).ok();
1018 }
1019
1020 #[test]
1021 fn field_id_is_stable_and_reopenable() {
1022 let root = temp_root("reopen");
1023 let bytes = tiny_descriptor();
1024 let id1 = {
1025 let mut store = FieldStore::open(&root).unwrap();
1026 store.ingest(&bytes, Limits::DEFAULT).unwrap()
1027 };
1028 let store = FieldStore::open(&root).unwrap();
1029 let m = store.get_field(&id1).unwrap();
1030 assert_eq!(m.content_id(), id1);
1031 assert_eq!(store.list_fields().unwrap(), vec![id1]);
1032 fs::remove_dir_all(&root).ok();
1033 }
1034
1035 #[test]
1036 fn promotion_is_a_noop_by_default() {
1037 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1038 let root = temp_root("promote-off");
1039 let mut store = FieldStore::open(&root).unwrap();
1040 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
1041 let req = ObserveRequest::new(
1042 Selector::ByteRange { offset: 4, len: 5 },
1043 Representation::ExactBytes,
1044 );
1045 let (a1, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1046 let (a2, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1047 assert_eq!(a1, a2);
1048 assert!(!root.join("promoted").exists(), "promotion must be opt-in");
1050 fs::remove_dir_all(&root).ok();
1051 }
1052
1053 #[test]
1054 fn promotion_preserves_the_answer_and_the_durable_store_serves_it() {
1055 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1056 use crate::field::promote::PromotePolicy;
1057 let root = temp_root("promote-on");
1058 let mut store = FieldStore::open(&root).unwrap();
1059 store.set_promote(PromotePolicy {
1061 enabled: true,
1062 min_hits: 1,
1063 rent_per_byte: 0,
1064 ..PromotePolicy::default()
1065 });
1066 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
1067 let req = ObserveRequest::new(
1068 Selector::ByteRange { offset: 4, len: 5 },
1069 Representation::ExactBytes,
1070 );
1071 let (cold, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1072 let (warm, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1073 assert_eq!(cold, warm, "promotion must not change the answer");
1074 let promoted = store.promoted().unwrap();
1076 let ob = promoted.output_bytes().unwrap();
1077 assert!(
1078 ob > 0,
1079 "the reused node must be in the durable store (output_bytes={ob}, promoted_dir_exists={})",
1080 root.join("promoted").exists()
1081 );
1082 crate::field::cache::DerivedCache::open(root.join("cache"))
1084 .unwrap()
1085 .clear()
1086 .unwrap();
1087 let (after, _, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1088 assert_eq!(cold, after, "the promoted store must yield the same answer");
1089 fs::remove_dir_all(&root).ok();
1090 }
1091}
1092
1093#[cfg(all(test, feature = "entropyfs-store"))]
1096mod entropyfs_field_tests {
1097 use super::*;
1098 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1099 use crate::field::provenance::AnswerValue;
1100 use crate::store::seed_closure;
1101
1102 fn temp_root(label: &str) -> PathBuf {
1103 let mut p = std::env::temp_dir();
1104 p.push(format!(
1105 "vole-field-entropyfs-{label}-{}-{}",
1106 std::process::id(),
1107 std::time::SystemTime::now()
1108 .duration_since(std::time::UNIX_EPOCH)
1109 .unwrap()
1110 .as_nanos()
1111 ));
1112 p
1113 }
1114
1115 fn tiny_descriptor() -> Vec<u8> {
1116 use crate::container::{Descriptor, ObjectSource};
1117 use crate::dra::{Op, Program};
1118 let source = b"the exact field bytes";
1119 let d = Descriptor {
1120 universe: crate::container::UNIVERSE.to_string(),
1121 source_format: crate::SOURCE_FORMAT_OPAQUE,
1122 format_basis: "opaque:field-entropyfs-test".to_string(),
1123 models: vec![],
1124 channels: vec![],
1125 objects: vec![ObjectSource::Inline(source.to_vec())],
1126 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
1127 observation_index: None,
1128 seek_directory: false,
1129 checkpoints: None,
1130 source_sha256: crate::integrity::sha256(source),
1131 source_len: source.len() as u64,
1132 };
1133 d.serialize().unwrap().0
1134 }
1135
1136 #[test]
1137 fn ingest_observe_and_exact_materialize_through_the_engine() {
1138 let root = temp_root("rt");
1139 let descriptor = tiny_descriptor();
1140 let expected = b"the exact field bytes";
1141 let mut store = FieldStore::open_entropyfs(&root).unwrap();
1142 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1143 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1144 assert_eq!(field.materialize_exact(Limits::DEFAULT).unwrap(), expected);
1145 let mut budget = dag::EvalBudget::default();
1147 assert_eq!(
1148 field
1149 .materialize_node(&field.manifest().root_node, Limits::DEFAULT, &mut budget)
1150 .unwrap(),
1151 expected
1152 );
1153 let req = ObserveRequest::new(
1155 Selector::ByteRange { offset: 4, len: 5 },
1156 Representation::ExactBytes,
1157 );
1158 let (answer, _stats, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
1159 match answer.value {
1160 AnswerValue::Bytes(b) => assert_eq!(b, b"exact"),
1161 other => panic!("expected bytes, got {other:?}"),
1162 }
1163 assert_eq!(
1166 store.list_fields().unwrap_err().class(),
1167 crate::ErrorClass::UnsupportedFeature
1168 );
1169 std::fs::remove_dir_all(&root).ok();
1170 }
1171
1172 #[test]
1173 fn cross_process_reopen_reads_what_a_previous_handle_wrote() {
1174 let root = temp_root("reopen");
1175 let descriptor = tiny_descriptor();
1176 let (id, root_node) = {
1179 let mut store = FieldStore::open_entropyfs(&root).unwrap();
1180 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1181 let node = store.get_field(&id).unwrap().root_node;
1182 store.sync().unwrap();
1184 (id, node)
1185 };
1186 let store = FieldStore::open_entropyfs(&root).unwrap();
1188 let manifest = store.get_field(&id).unwrap();
1189 assert_eq!(manifest.content_id(), id);
1190 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1191 assert_eq!(
1192 field.materialize_exact(Limits::DEFAULT).unwrap(),
1193 b"the exact field bytes"
1194 );
1195 let mut budget = dag::EvalBudget::default();
1196 assert_eq!(
1197 field
1198 .materialize_node(&root_node, Limits::DEFAULT, &mut budget)
1199 .unwrap(),
1200 b"the exact field bytes"
1201 );
1202 std::fs::remove_dir_all(&root).ok();
1203 }
1204
1205 #[test]
1206 fn list_nodes_declines_so_gc_cannot_sweep() {
1207 let root = temp_root("gc");
1208 let mut store = FieldStore::open_entropyfs(&root).unwrap();
1209 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
1210 let manifest = store.get_field(&id).unwrap();
1211 let e = store.seeds().list_nodes().unwrap_err();
1213 assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
1214 let reachable =
1217 seed_closure(store.seeds(), &[manifest.root_node], |_| Ok(Vec::new())).unwrap();
1218 assert!(reachable.contains(&manifest.root_node));
1219 std::fs::remove_dir_all(&root).ok();
1220 }
1221}
1222
1223#[cfg(test)]
1227mod packed_field_tests {
1228 use super::*;
1229 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
1230 use crate::field::provenance::AnswerValue;
1231
1232 fn temp_root(label: &str) -> PathBuf {
1233 let mut p = std::env::temp_dir();
1234 p.push(format!(
1235 "vole-field-packed-{label}-{}-{}",
1236 std::process::id(),
1237 std::time::SystemTime::now()
1238 .duration_since(std::time::UNIX_EPOCH)
1239 .unwrap()
1240 .as_nanos()
1241 ));
1242 p
1243 }
1244
1245 fn tiny_descriptor() -> Vec<u8> {
1246 use crate::container::{Descriptor, ObjectSource};
1247 use crate::dra::{Op, Program};
1248 let source = b"the exact field bytes";
1249 let d = Descriptor {
1250 universe: crate::container::UNIVERSE.to_string(),
1251 source_format: crate::SOURCE_FORMAT_OPAQUE,
1252 format_basis: "opaque:field-packed-test".to_string(),
1253 models: vec![],
1254 channels: vec![],
1255 objects: vec![ObjectSource::Inline(source.to_vec())],
1256 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
1257 observation_index: None,
1258 seek_directory: false,
1259 checkpoints: None,
1260 source_sha256: crate::integrity::sha256(source),
1261 source_len: source.len() as u64,
1262 };
1263 d.serialize().unwrap().0
1264 }
1265
1266 #[test]
1267 fn packed_matches_fs_for_ingest_observe_and_exact_materialize() {
1268 let fs_root = temp_root("fs");
1269 let pk_root = temp_root("packed");
1270 let descriptor = tiny_descriptor();
1271 let expected = b"the exact field bytes";
1272
1273 let mut fs_store = FieldStore::open(&fs_root).unwrap();
1274 let fs_id = fs_store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1275 fs_store.sync().unwrap();
1276
1277 let mut pk_store = FieldStore::open_packed(&pk_root).unwrap();
1278 let pk_id = pk_store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1279 pk_store.sync().unwrap();
1280
1281 assert_eq!(
1284 fs_id, pk_id,
1285 "the packed backend must not change the FieldId"
1286 );
1287 assert_eq!(
1288 fs_store.get_field(&fs_id).unwrap().root_node,
1289 pk_store.get_field(&pk_id).unwrap().root_node,
1290 "node identity is content-derived and backend-independent"
1291 );
1292
1293 {
1295 let f = Field::open(&fs_store, &fs_id, Limits::DEFAULT).unwrap();
1296 assert_eq!(f.materialize_exact(Limits::DEFAULT).unwrap(), expected);
1297 let p = Field::open(&pk_store, &pk_id, Limits::DEFAULT).unwrap();
1298 assert_eq!(p.materialize_exact(Limits::DEFAULT).unwrap(), expected);
1299 }
1300
1301 let req = ObserveRequest::new(
1303 Selector::ByteRange { offset: 4, len: 5 },
1304 Representation::ExactBytes,
1305 );
1306 let (fs_ans, _, _) =
1307 observe::observe(&mut fs_store, &fs_id, &req, Limits::DEFAULT).unwrap();
1308 let (pk_ans, _, _) =
1309 observe::observe(&mut pk_store, &pk_id, &req, Limits::DEFAULT).unwrap();
1310 match (&fs_ans.value, &pk_ans.value) {
1311 (AnswerValue::Bytes(a), AnswerValue::Bytes(b)) => {
1312 assert_eq!(a, b, "packed and fs observations must agree");
1313 assert_eq!(b, b"exact");
1314 }
1315 other => panic!("expected byte answers, got {other:?}"),
1316 }
1317
1318 std::fs::remove_dir_all(&fs_root).ok();
1319 std::fs::remove_dir_all(&pk_root).ok();
1320 }
1321
1322 #[test]
1323 fn packed_reopen_serves_the_same_field() {
1324 let root = temp_root("reopen");
1325 let descriptor = tiny_descriptor();
1326 let id = {
1327 let mut store = FieldStore::open_packed(&root).unwrap();
1328 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
1329 store.sync().unwrap();
1331 id
1332 };
1333 let store = FieldStore::open_packed(&root).unwrap();
1334 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
1335 assert_eq!(
1336 field.materialize_exact(Limits::DEFAULT).unwrap(),
1337 b"the exact field bytes"
1338 );
1339 std::fs::remove_dir_all(&root).ok();
1340 }
1341}