1pub mod cache;
16pub mod capabilities;
17pub mod dag;
18pub mod derive;
19pub mod document_format;
20pub mod edit;
21pub mod explain;
22pub mod index;
23pub mod ingest;
24#[cfg(feature = "package")]
25pub mod ingest_package;
26pub mod manifest;
27pub mod node;
28pub mod observe;
29#[cfg(feature = "opc")]
30pub mod opc;
31pub mod partial;
32pub mod plan;
33pub mod provenance;
34pub mod resource;
35pub mod share;
36
37pub use manifest::{FieldId, FieldRoot};
38
39use std::fs;
40use std::path::{Path, PathBuf};
41
42#[cfg(feature = "entropyfs-store")]
43use std::sync::Arc;
44
45#[cfg(feature = "entropyfs-store")]
46use entropyfs::engine::BlobId;
47
48use crate::container::ParsedDescriptor;
49use crate::error::{Error, Result};
50use crate::limits::Limits;
51#[cfg(feature = "entropyfs-store")]
52use crate::store::{EntropyFsStore, map_engine_error};
53use crate::store::{FsSeedStore, Id, IoCounters, IoSnapshot, NodeId, SeedStore};
54
55#[cfg(feature = "entropyfs-store")]
56use self::manifest::FIELD_ROOT_DOMAIN;
57use self::node::{NodeKind, SeedNode};
58
59pub 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";
61
62#[cfg(feature = "package")]
69pub 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";
70
71#[derive(Clone)]
80pub(crate) enum SeedSubstrate {
81 Fs { root: PathBuf, io: IoCounters },
83 #[cfg(feature = "entropyfs-store")]
85 EntropyFs {
86 store: Arc<EntropyFsStore>,
87 io: IoCounters,
88 },
89}
90
91impl SeedStore for SeedSubstrate {
92 fn put_node(&mut self, canonical: &[u8]) -> Result<NodeId> {
93 match self {
94 SeedSubstrate::Fs { root, io } => {
95 FsSeedStore::open_with_io(root, io.handle())?.put_node(canonical)
96 }
97 #[cfg(feature = "entropyfs-store")]
98 SeedSubstrate::EntropyFs { store, .. } => store.seed_put(canonical),
99 }
100 }
101
102 fn get_node(&self, id: &NodeId) -> Result<Vec<u8>> {
103 match self {
104 SeedSubstrate::Fs { root, io } => {
105 FsSeedStore::open_with_io(root, io.handle())?.get_node(id)
106 }
107 #[cfg(feature = "entropyfs-store")]
108 SeedSubstrate::EntropyFs { store, io } => {
109 let bytes = store.seed_get(id)?;
110 io.add_seed(bytes.len() as u64);
111 Ok(bytes)
112 }
113 }
114 }
115
116 fn get_node_range(&self, id: &NodeId, offset: u64, len: u64) -> Result<Vec<u8>> {
117 match self {
118 SeedSubstrate::Fs { root, io } => {
119 FsSeedStore::open_with_io(root, io.handle())?.get_node_range(id, offset, len)
120 }
121 #[cfg(feature = "entropyfs-store")]
122 SeedSubstrate::EntropyFs { store, io } => {
123 let bytes = store.seed_get_range(id, offset, len)?;
124 io.add_seed(bytes.len() as u64);
125 Ok(bytes)
126 }
127 }
128 }
129
130 fn contains_node(&self, id: &NodeId) -> Result<bool> {
131 match self {
132 SeedSubstrate::Fs { root, io } => {
133 FsSeedStore::open_with_io(root, io.handle())?.contains_node(id)
134 }
135 #[cfg(feature = "entropyfs-store")]
136 SeedSubstrate::EntropyFs { store, .. } => store.seed_contains(id),
137 }
138 }
139
140 fn list_nodes(&self) -> Result<Vec<(NodeId, u64)>> {
141 match self {
142 SeedSubstrate::Fs { root, io } => {
143 FsSeedStore::open_with_io(root, io.handle())?.list_nodes()
144 }
145 #[cfg(feature = "entropyfs-store")]
146 SeedSubstrate::EntropyFs { store, .. } => store.seed_list(),
147 }
148 }
149}
150
151enum BlobBackend {
159 Fs,
161 #[cfg(feature = "entropyfs-store")]
167 EntropyFs(Arc<EntropyFsStore>),
168}
169
170impl BlobBackend {
171 fn descriptor_put(&self, root: &Path, bytes: &[u8]) -> Result<Id> {
172 match self {
173 BlobBackend::Fs => {
174 let id = Id::of(bytes);
175 let path = root.join("descriptor").join(id.to_hex());
176 if !path.exists() {
177 write_atomic(&path, bytes)?;
178 }
179 Ok(id)
180 }
181 #[cfg(feature = "entropyfs-store")]
182 BlobBackend::EntropyFs(store) => {
183 let id = Id::of(bytes);
184 let blob = store
185 .engine()
186 .put_blob(bytes)
187 .map_err(|e| map_engine_error("put_blob", &e))?;
188 debug_assert_eq!(blob.as_bytes(), id.as_bytes());
189 Ok(id)
190 }
191 }
192 }
193
194 fn descriptor_get(&self, root: &Path, id: &Id) -> Result<Vec<u8>> {
195 match self {
196 BlobBackend::Fs => {
197 let path = root.join("descriptor").join(id.to_hex());
198 fs::read(&path).map_err(|e| {
199 if e.kind() == std::io::ErrorKind::NotFound {
200 Error::missing_external_object(format!(
201 "descriptor blob {id} is not present"
202 ))
203 } else {
204 Error::io(format!("reading descriptor blob {id}: {e}"))
205 }
206 })
207 }
208 #[cfg(feature = "entropyfs-store")]
209 BlobBackend::EntropyFs(store) => store
210 .engine()
211 .get_blob(BlobId::new(*id.as_bytes()))
212 .map_err(|e| map_engine_error("get_blob", &e)),
213 }
214 }
215
216 fn field_put(&self, root: &Path, manifest: &FieldRoot) -> Result<FieldId> {
217 let bytes = manifest.encode_canonical();
218 let id = FieldId::of_manifest(&bytes);
219 match self {
220 BlobBackend::Fs => {
221 let path = root.join("field").join(id.to_hex());
222 if !path.exists() {
223 write_atomic(&path, &bytes)?;
224 }
225 Ok(id)
226 }
227 #[cfg(feature = "entropyfs-store")]
228 BlobBackend::EntropyFs(store) => {
229 let mut prefixed = Vec::with_capacity(FIELD_ROOT_DOMAIN.len() + bytes.len());
230 prefixed.extend_from_slice(FIELD_ROOT_DOMAIN);
231 prefixed.extend_from_slice(&bytes);
232 let blob = store
233 .engine()
234 .put_blob(&prefixed)
235 .map_err(|e| map_engine_error("put_blob", &e))?;
236 debug_assert_eq!(blob.as_bytes(), id.as_bytes());
237 Ok(id)
238 }
239 }
240 }
241
242 fn field_get(&self, root: &Path, id: &FieldId) -> Result<Vec<u8>> {
243 match self {
244 BlobBackend::Fs => {
245 let path = root.join("field").join(id.to_hex());
246 fs::read(&path).map_err(|e| {
247 if e.kind() == std::io::ErrorKind::NotFound {
248 Error::missing_external_object(format!("field {id} is not present"))
249 } else {
250 Error::io(format!("reading field {id}: {e}"))
251 }
252 })
253 }
254 #[cfg(feature = "entropyfs-store")]
255 BlobBackend::EntropyFs(store) => {
256 let blob = store
257 .engine()
258 .get_blob(BlobId::new(*id.as_bytes()))
259 .map_err(|e| map_engine_error("get_blob", &e))?;
260 let rest = blob.strip_prefix(FIELD_ROOT_DOMAIN).ok_or_else(|| {
261 Error::integrity_mismatch(format!(
262 "field manifest {id} is missing its domain prefix"
263 ))
264 })?;
265 Ok(rest.to_vec())
266 }
267 }
268 }
269}
270
271#[cfg(feature = "entropyfs-store")]
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub struct EngineStoreStats {
295 pub blob_count: u64,
297 pub logical_bytes: u64,
299 pub physical_used_bytes: u64,
301}
302
303pub struct FieldStore {
304 root: PathBuf,
305 backend: BlobBackend,
306 seeds: SeedSubstrate,
307 io: IoCounters,
308}
309
310impl std::fmt::Debug for FieldStore {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.debug_struct("FieldStore")
313 .field("root", &self.root)
314 .finish_non_exhaustive()
315 }
316}
317
318impl FieldStore {
319 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
321 let root = root.as_ref().to_path_buf();
322 fs::create_dir_all(root.join("descriptor"))?;
323 fs::create_dir_all(root.join("field"))?;
324 fs::create_dir_all(root.join("index"))?;
325 fs::create_dir_all(root.join("cache"))?;
326 let io = IoCounters::new();
327 let seeds = SeedSubstrate::Fs {
328 root: root.clone(),
329 io: io.handle(),
330 };
331 Ok(FieldStore {
332 root,
333 backend: BlobBackend::Fs,
334 seeds,
335 io,
336 })
337 }
338
339 #[cfg(feature = "entropyfs-store")]
349 pub fn open_entropyfs(root: impl AsRef<Path>) -> Result<Self> {
350 let root = root.as_ref().to_path_buf();
351 fs::create_dir_all(root.join("index"))?;
352 fs::create_dir_all(root.join("cache"))?;
353 let engine_root = root.join("entropyfs");
354 fs::create_dir_all(&engine_root)?;
355 let fresh = fs::read_dir(&engine_root)?.next().is_none();
358 let engine = if fresh {
359 EntropyFsStore::create(&engine_root)?
360 } else {
361 EntropyFsStore::open(&engine_root)?
362 };
363 let engine = Arc::new(engine);
364 let io = IoCounters::new();
365 let seeds = SeedSubstrate::EntropyFs {
366 store: Arc::clone(&engine),
367 io: io.handle(),
368 };
369 Ok(FieldStore {
370 root,
371 backend: BlobBackend::EntropyFs(engine),
372 seeds,
373 io,
374 })
375 }
376
377 pub fn io(&self) -> &IoCounters {
383 &self.io
384 }
385
386 pub fn root(&self) -> &Path {
388 &self.root
389 }
390
391 pub(crate) fn seed_substrate(&self) -> SeedSubstrate {
394 self.seeds.clone()
395 }
396
397 pub(crate) fn supports_partial_descriptor(&self) -> bool {
401 matches!(self.backend, BlobBackend::Fs)
402 }
403
404 pub fn seeds(&self) -> &dyn SeedStore {
406 &self.seeds
407 }
408
409 pub fn seeds_mut(&mut self) -> &mut dyn SeedStore {
411 &mut self.seeds
412 }
413
414 pub fn cache(&self) -> Result<cache::DerivedCache> {
419 cache::DerivedCache::open(self.root.join("cache"))
420 }
421
422 pub fn sync(&self) -> Result<()> {
429 match &self.backend {
430 BlobBackend::Fs => Ok(()),
431 #[cfg(feature = "entropyfs-store")]
432 BlobBackend::EntropyFs(store) => store.sync(),
433 }
434 }
435
436 #[cfg(feature = "entropyfs-store")]
444 pub fn engine_stats(&self) -> Result<Option<EngineStoreStats>> {
445 match &self.backend {
446 BlobBackend::Fs => Ok(None),
447 BlobBackend::EntropyFs(store) => {
448 let m = store
449 .engine()
450 .metrics()
451 .map_err(|e| map_engine_error("metrics", &e))?;
452 Ok(Some(EngineStoreStats {
453 blob_count: m.accounting.blob_count,
454 logical_bytes: m.accounting.logical_bytes,
455 physical_used_bytes: m.accounting.physical_used_bytes,
456 }))
457 }
458 }
459 }
460
461 pub(crate) fn descriptor_path(&self, id: &Id) -> Option<PathBuf> {
464 match self.backend {
465 BlobBackend::Fs => Some(self.root.join("descriptor").join(id.to_hex())),
466 #[cfg(feature = "entropyfs-store")]
467 BlobBackend::EntropyFs(_) => None,
468 }
469 }
470
471 pub fn put_descriptor(&mut self, bytes: &[u8]) -> Result<Id> {
476 self.backend.descriptor_put(&self.root, bytes)
477 }
478
479 pub fn get_descriptor(&self, id: &Id) -> Result<Vec<u8>> {
481 let bytes = self.backend.descriptor_get(&self.root, id)?;
482 let actual = Id::of(&bytes);
483 if actual != *id {
484 return Err(Error::integrity_mismatch(format!(
485 "descriptor blob {id} hashes to {actual}"
486 )));
487 }
488 self.io.add_descriptor(bytes.len() as u64);
489 Ok(bytes)
490 }
491
492 pub fn put_field(&mut self, manifest: &FieldRoot) -> Result<FieldId> {
494 self.backend.field_put(&self.root, manifest)
495 }
496
497 pub fn get_field(&self, id: &FieldId) -> Result<FieldRoot> {
499 let bytes = self.backend.field_get(&self.root, id)?;
500 let manifest = FieldRoot::decode_canonical(&bytes)?;
501 if manifest.content_id() != *id {
502 return Err(Error::integrity_mismatch(format!(
503 "field {id} manifest content id mismatch"
504 )));
505 }
506 self.io.add_manifest(bytes.len() as u64);
507 Ok(manifest)
508 }
509
510 pub fn list_fields(&self) -> Result<Vec<FieldId>> {
517 if !matches!(self.backend, BlobBackend::Fs) {
518 return Err(Error::unsupported_feature(
519 "EntropyFS exposes no per-blob enumeration; a field store backed by it \
520 cannot list its manifests (open a field by its FieldId instead)",
521 ));
522 }
523 let dir = self.root.join("field");
524 let mut out = Vec::new();
525 for entry in fs::read_dir(&dir)?.flatten() {
526 if let Some(name) = entry.file_name().to_str()
527 && let Ok(id) = FieldId::from_hex(name)
528 {
529 out.push(id);
530 }
531 }
532 out.sort_unstable();
533 Ok(out)
534 }
535
536 pub fn ingest(&mut self, descriptor_bytes: &[u8], limits: Limits) -> Result<FieldId> {
542 let parsed = crate::container::Descriptor::parse(descriptor_bytes, limits)?;
545 let source = crate::materialize::materialize(&parsed, limits)?;
546 let descriptor_id = self.put_descriptor(descriptor_bytes)?;
547
548 let root = SeedNode::new(
549 NodeKind::DocumentExact,
550 source.len() as u64,
551 Vec::new(),
552 Vec::new(),
553 "field:document-exact",
554 );
555 let root_id = self.seeds.put_node(&root.encode_canonical())?;
556
557 let manifest = FieldRoot {
558 universe_id: crate::container::universe_id_from_str(FIELD_UNIVERSE),
559 source_sha256: parsed.descriptor.source_sha256,
560 source_len: parsed.descriptor.source_len,
561 descriptor_id: *descriptor_id.as_bytes(),
562 root_node: root_id,
563 index_root: manifest::ABSENT_ROOT,
564 node_count: 1,
565 index_node_count: 0,
566 provenance: format!("field:ingest;{}", parsed.descriptor.format_basis),
567 };
568 self.put_field(&manifest)
569 }
570}
571
572pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
574 use std::io::Write;
575 let dir = path
576 .parent()
577 .ok_or_else(|| Error::internal_invariant("atomic write path has no parent"))?;
578 fs::create_dir_all(dir)?;
579 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("blob");
580 let tmp = dir.join(format!(".{name}.tmp-{}", std::process::id()));
581 {
582 let mut f = fs::File::create(&tmp)?;
583 f.write_all(bytes)?;
584 f.sync_all()?;
585 }
586 fs::rename(&tmp, path)?;
587 Ok(())
588}
589
590pub struct Field {
592 store_root: PathBuf,
593 manifest: FieldRoot,
594 parsed: ParsedDescriptor,
595 descriptor_bytes: Vec<u8>,
596 seeds: SeedSubstrate,
600 open_io: IoSnapshot,
604}
605
606impl std::fmt::Debug for Field {
607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 f.debug_struct("Field")
609 .field("manifest", &self.manifest.content_id())
610 .field("source_len", &self.manifest.source_len)
611 .finish_non_exhaustive()
612 }
613}
614
615impl Field {
616 pub fn open(store: &FieldStore, id: &FieldId, limits: Limits) -> Result<Field> {
618 let io_before = store.io().snapshot();
619 let manifest = store.get_field(id)?;
620 Field::open_after_manifest(store, manifest, io_before, limits)
621 }
622
623 pub(crate) fn open_after_manifest(
628 store: &FieldStore,
629 manifest: FieldRoot,
630 io_before: IoSnapshot,
631 limits: Limits,
632 ) -> Result<Field> {
633 let descriptor_bytes = store.get_descriptor(&Id::from_bytes(manifest.descriptor_id))?;
634 let open_io = io_before.delta(&store.io().snapshot());
635 let mut parsed = crate::container::Descriptor::parse(&descriptor_bytes, limits)?;
636 parsed.universe_id = manifest.universe_id;
637 if parsed.descriptor.source_len != manifest.source_len
639 || parsed.descriptor.source_sha256 != manifest.source_sha256
640 {
641 return Err(Error::integrity_mismatch(
642 "field manifest does not match its descriptor's declared source",
643 ));
644 }
645 Ok(Field {
646 store_root: store.root().to_path_buf(),
647 manifest,
648 parsed,
649 descriptor_bytes,
650 seeds: store.seed_substrate(),
651 open_io,
652 })
653 }
654
655 pub(crate) fn open_io(&self) -> IoSnapshot {
657 self.open_io
658 }
659
660 pub fn manifest(&self) -> &FieldRoot {
662 &self.manifest
663 }
664
665 pub fn id(&self) -> FieldId {
667 self.manifest.content_id()
668 }
669
670 pub fn parsed(&self) -> &ParsedDescriptor {
672 &self.parsed
673 }
674
675 pub fn descriptor_bytes(&self) -> &[u8] {
677 &self.descriptor_bytes
678 }
679
680 pub fn store_root(&self) -> &Path {
682 &self.store_root
683 }
684
685 pub fn materialize_exact(&self, limits: Limits) -> Result<Vec<u8>> {
691 crate::materialize::materialize(&self.parsed, limits)
692 }
693
694 pub fn materialize_node(
697 &self,
698 id: &NodeId,
699 limits: Limits,
700 budget: &mut dag::EvalBudget,
701 ) -> Result<Vec<u8>> {
702 let node = dag::load_node(&self.seeds, id)?;
703 dag::materialize_node(
704 &self.parsed,
705 &self.seeds,
706 &node,
707 limits,
708 budget,
709 node.limits.max_depth,
710 )
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 fn temp_root(label: &str) -> PathBuf {
719 let mut p = std::env::temp_dir();
720 p.push(format!(
721 "vole-field-{label}-{}-{}",
722 std::process::id(),
723 std::time::SystemTime::now()
724 .duration_since(std::time::UNIX_EPOCH)
725 .unwrap()
726 .as_nanos()
727 ));
728 p
729 }
730
731 fn tiny_descriptor() -> Vec<u8> {
732 use crate::container::{Descriptor, ObjectSource};
733 use crate::dra::{Op, Program};
734 let source = b"the exact field bytes";
735 let d = Descriptor {
736 universe: crate::container::UNIVERSE.to_string(),
737 source_format: crate::SOURCE_FORMAT_OPAQUE,
738 format_basis: "opaque:field-test".to_string(),
739 models: vec![],
740 channels: vec![],
741 objects: vec![ObjectSource::Inline(source.to_vec())],
742 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
743 observation_index: None,
744 seek_directory: false,
745 source_sha256: crate::integrity::sha256(source),
746 source_len: source.len() as u64,
747 };
748 d.serialize().unwrap().0
749 }
750
751 #[test]
752 fn ingest_then_materialize_is_exact() {
753 let root = temp_root("rt");
754 let mut store = FieldStore::open(&root).unwrap();
755 let bytes = tiny_descriptor();
756 let id = store.ingest(&bytes, Limits::DEFAULT).unwrap();
757 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
758 assert_eq!(
759 field.materialize_exact(Limits::DEFAULT).unwrap(),
760 b"the exact field bytes"
761 );
762 let mut budget = dag::EvalBudget::default();
764 let out = field
765 .materialize_node(&field.manifest.root_node, Limits::DEFAULT, &mut budget)
766 .unwrap();
767 assert_eq!(out, b"the exact field bytes");
768 fs::remove_dir_all(&root).ok();
769 }
770
771 #[test]
772 fn field_id_is_stable_and_reopenable() {
773 let root = temp_root("reopen");
774 let bytes = tiny_descriptor();
775 let id1 = {
776 let mut store = FieldStore::open(&root).unwrap();
777 store.ingest(&bytes, Limits::DEFAULT).unwrap()
778 };
779 let store = FieldStore::open(&root).unwrap();
780 let m = store.get_field(&id1).unwrap();
781 assert_eq!(m.content_id(), id1);
782 assert_eq!(store.list_fields().unwrap(), vec![id1]);
783 fs::remove_dir_all(&root).ok();
784 }
785}
786
787#[cfg(all(test, feature = "entropyfs-store"))]
790mod entropyfs_field_tests {
791 use super::*;
792 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
793 use crate::field::provenance::AnswerValue;
794 use crate::store::seed_closure;
795
796 fn temp_root(label: &str) -> PathBuf {
797 let mut p = std::env::temp_dir();
798 p.push(format!(
799 "vole-field-entropyfs-{label}-{}-{}",
800 std::process::id(),
801 std::time::SystemTime::now()
802 .duration_since(std::time::UNIX_EPOCH)
803 .unwrap()
804 .as_nanos()
805 ));
806 p
807 }
808
809 fn tiny_descriptor() -> Vec<u8> {
810 use crate::container::{Descriptor, ObjectSource};
811 use crate::dra::{Op, Program};
812 let source = b"the exact field bytes";
813 let d = Descriptor {
814 universe: crate::container::UNIVERSE.to_string(),
815 source_format: crate::SOURCE_FORMAT_OPAQUE,
816 format_basis: "opaque:field-entropyfs-test".to_string(),
817 models: vec![],
818 channels: vec![],
819 objects: vec![ObjectSource::Inline(source.to_vec())],
820 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
821 observation_index: None,
822 seek_directory: false,
823 source_sha256: crate::integrity::sha256(source),
824 source_len: source.len() as u64,
825 };
826 d.serialize().unwrap().0
827 }
828
829 #[test]
830 fn ingest_observe_and_exact_materialize_through_the_engine() {
831 let root = temp_root("rt");
832 let descriptor = tiny_descriptor();
833 let expected = b"the exact field bytes";
834 let mut store = FieldStore::open_entropyfs(&root).unwrap();
835 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
836 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
837 assert_eq!(field.materialize_exact(Limits::DEFAULT).unwrap(), expected);
838 let mut budget = dag::EvalBudget::default();
840 assert_eq!(
841 field
842 .materialize_node(&field.manifest().root_node, Limits::DEFAULT, &mut budget)
843 .unwrap(),
844 expected
845 );
846 let req = ObserveRequest::new(
848 Selector::ByteRange { offset: 4, len: 5 },
849 Representation::ExactBytes,
850 );
851 let (answer, _stats, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
852 match answer.value {
853 AnswerValue::Bytes(b) => assert_eq!(b, b"exact"),
854 other => panic!("expected bytes, got {other:?}"),
855 }
856 assert_eq!(
859 store.list_fields().unwrap_err().class(),
860 crate::ErrorClass::UnsupportedFeature
861 );
862 std::fs::remove_dir_all(&root).ok();
863 }
864
865 #[test]
866 fn cross_process_reopen_reads_what_a_previous_handle_wrote() {
867 let root = temp_root("reopen");
868 let descriptor = tiny_descriptor();
869 let (id, root_node) = {
872 let mut store = FieldStore::open_entropyfs(&root).unwrap();
873 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
874 let node = store.get_field(&id).unwrap().root_node;
875 store.sync().unwrap();
877 (id, node)
878 };
879 let store = FieldStore::open_entropyfs(&root).unwrap();
881 let manifest = store.get_field(&id).unwrap();
882 assert_eq!(manifest.content_id(), id);
883 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
884 assert_eq!(
885 field.materialize_exact(Limits::DEFAULT).unwrap(),
886 b"the exact field bytes"
887 );
888 let mut budget = dag::EvalBudget::default();
889 assert_eq!(
890 field
891 .materialize_node(&root_node, Limits::DEFAULT, &mut budget)
892 .unwrap(),
893 b"the exact field bytes"
894 );
895 std::fs::remove_dir_all(&root).ok();
896 }
897
898 #[test]
899 fn list_nodes_declines_so_gc_cannot_sweep() {
900 let root = temp_root("gc");
901 let mut store = FieldStore::open_entropyfs(&root).unwrap();
902 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
903 let manifest = store.get_field(&id).unwrap();
904 let e = store.seeds().list_nodes().unwrap_err();
906 assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
907 let reachable =
910 seed_closure(store.seeds(), &[manifest.root_node], |_| Ok(Vec::new())).unwrap();
911 assert!(reachable.contains(&manifest.root_node));
912 std::fs::remove_dir_all(&root).ok();
913 }
914}