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 checkpoints: None,
746 source_sha256: crate::integrity::sha256(source),
747 source_len: source.len() as u64,
748 };
749 d.serialize().unwrap().0
750 }
751
752 #[test]
753 fn ingest_then_materialize_is_exact() {
754 let root = temp_root("rt");
755 let mut store = FieldStore::open(&root).unwrap();
756 let bytes = tiny_descriptor();
757 let id = store.ingest(&bytes, Limits::DEFAULT).unwrap();
758 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
759 assert_eq!(
760 field.materialize_exact(Limits::DEFAULT).unwrap(),
761 b"the exact field bytes"
762 );
763 let mut budget = dag::EvalBudget::default();
765 let out = field
766 .materialize_node(&field.manifest.root_node, Limits::DEFAULT, &mut budget)
767 .unwrap();
768 assert_eq!(out, b"the exact field bytes");
769 fs::remove_dir_all(&root).ok();
770 }
771
772 #[test]
773 fn field_id_is_stable_and_reopenable() {
774 let root = temp_root("reopen");
775 let bytes = tiny_descriptor();
776 let id1 = {
777 let mut store = FieldStore::open(&root).unwrap();
778 store.ingest(&bytes, Limits::DEFAULT).unwrap()
779 };
780 let store = FieldStore::open(&root).unwrap();
781 let m = store.get_field(&id1).unwrap();
782 assert_eq!(m.content_id(), id1);
783 assert_eq!(store.list_fields().unwrap(), vec![id1]);
784 fs::remove_dir_all(&root).ok();
785 }
786}
787
788#[cfg(all(test, feature = "entropyfs-store"))]
791mod entropyfs_field_tests {
792 use super::*;
793 use crate::field::observe::{self, ObserveRequest, Representation, Selector};
794 use crate::field::provenance::AnswerValue;
795 use crate::store::seed_closure;
796
797 fn temp_root(label: &str) -> PathBuf {
798 let mut p = std::env::temp_dir();
799 p.push(format!(
800 "vole-field-entropyfs-{label}-{}-{}",
801 std::process::id(),
802 std::time::SystemTime::now()
803 .duration_since(std::time::UNIX_EPOCH)
804 .unwrap()
805 .as_nanos()
806 ));
807 p
808 }
809
810 fn tiny_descriptor() -> Vec<u8> {
811 use crate::container::{Descriptor, ObjectSource};
812 use crate::dra::{Op, Program};
813 let source = b"the exact field bytes";
814 let d = Descriptor {
815 universe: crate::container::UNIVERSE.to_string(),
816 source_format: crate::SOURCE_FORMAT_OPAQUE,
817 format_basis: "opaque:field-entropyfs-test".to_string(),
818 models: vec![],
819 channels: vec![],
820 objects: vec![ObjectSource::Inline(source.to_vec())],
821 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
822 observation_index: None,
823 seek_directory: false,
824 checkpoints: None,
825 source_sha256: crate::integrity::sha256(source),
826 source_len: source.len() as u64,
827 };
828 d.serialize().unwrap().0
829 }
830
831 #[test]
832 fn ingest_observe_and_exact_materialize_through_the_engine() {
833 let root = temp_root("rt");
834 let descriptor = tiny_descriptor();
835 let expected = b"the exact field bytes";
836 let mut store = FieldStore::open_entropyfs(&root).unwrap();
837 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
838 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
839 assert_eq!(field.materialize_exact(Limits::DEFAULT).unwrap(), expected);
840 let mut budget = dag::EvalBudget::default();
842 assert_eq!(
843 field
844 .materialize_node(&field.manifest().root_node, Limits::DEFAULT, &mut budget)
845 .unwrap(),
846 expected
847 );
848 let req = ObserveRequest::new(
850 Selector::ByteRange { offset: 4, len: 5 },
851 Representation::ExactBytes,
852 );
853 let (answer, _stats, _) = observe::observe(&mut store, &id, &req, Limits::DEFAULT).unwrap();
854 match answer.value {
855 AnswerValue::Bytes(b) => assert_eq!(b, b"exact"),
856 other => panic!("expected bytes, got {other:?}"),
857 }
858 assert_eq!(
861 store.list_fields().unwrap_err().class(),
862 crate::ErrorClass::UnsupportedFeature
863 );
864 std::fs::remove_dir_all(&root).ok();
865 }
866
867 #[test]
868 fn cross_process_reopen_reads_what_a_previous_handle_wrote() {
869 let root = temp_root("reopen");
870 let descriptor = tiny_descriptor();
871 let (id, root_node) = {
874 let mut store = FieldStore::open_entropyfs(&root).unwrap();
875 let id = store.ingest(&descriptor, Limits::DEFAULT).unwrap();
876 let node = store.get_field(&id).unwrap().root_node;
877 store.sync().unwrap();
879 (id, node)
880 };
881 let store = FieldStore::open_entropyfs(&root).unwrap();
883 let manifest = store.get_field(&id).unwrap();
884 assert_eq!(manifest.content_id(), id);
885 let field = Field::open(&store, &id, Limits::DEFAULT).unwrap();
886 assert_eq!(
887 field.materialize_exact(Limits::DEFAULT).unwrap(),
888 b"the exact field bytes"
889 );
890 let mut budget = dag::EvalBudget::default();
891 assert_eq!(
892 field
893 .materialize_node(&root_node, Limits::DEFAULT, &mut budget)
894 .unwrap(),
895 b"the exact field bytes"
896 );
897 std::fs::remove_dir_all(&root).ok();
898 }
899
900 #[test]
901 fn list_nodes_declines_so_gc_cannot_sweep() {
902 let root = temp_root("gc");
903 let mut store = FieldStore::open_entropyfs(&root).unwrap();
904 let id = store.ingest(&tiny_descriptor(), Limits::DEFAULT).unwrap();
905 let manifest = store.get_field(&id).unwrap();
906 let e = store.seeds().list_nodes().unwrap_err();
908 assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
909 let reachable =
912 seed_closure(store.seeds(), &[manifest.root_node], |_| Ok(Vec::new())).unwrap();
913 assert!(reachable.contains(&manifest.root_node));
914 std::fs::remove_dir_all(&root).ok();
915 }
916}