use std::fs;
use std::path::{Path, PathBuf};
use entropyfs::engine::{BlobId, Engine, EngineError, EngineOpenOptions};
use crate::error::{Error, Result};
use crate::store::{Id, ObjectStore};
#[cfg(feature = "field")]
use crate::store::{NodeId, SEED_NODE_DOMAIN, SeedStore};
pub struct EntropyFsStore {
engine: Engine,
root: PathBuf,
}
impl std::fmt::Debug for EntropyFsStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntropyFsStore")
.field("root", &self.root)
.finish_non_exhaustive()
}
}
impl EntropyFsStore {
pub fn create(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref().to_path_buf();
fs::create_dir_all(&root)?;
let engine = Engine::create(&root, &EngineOpenOptions::default())
.map_err(|e| map_engine_error("create", &e))?;
Ok(EntropyFsStore { engine, root })
}
pub fn open(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref().to_path_buf();
let engine = Engine::open(&root, &EngineOpenOptions::default())
.map_err(|e| map_engine_error("open", &e))?;
Ok(EntropyFsStore { engine, root })
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn engine(&self) -> &Engine {
&self.engine
}
pub fn sync(&self) -> Result<()> {
self.engine.sync().map_err(|e| map_engine_error("sync", &e))
}
}
impl ObjectStore for EntropyFsStore {
fn put(&mut self, bytes: &[u8]) -> Result<Id> {
let blob = self
.engine
.put_blob(bytes)
.map_err(|e| map_engine_error("put_blob", &e))?;
let id = Id::from_bytes(*blob.as_bytes());
debug_assert_eq!(
id,
Id::of(bytes),
"EntropyFS BlobId must equal Id::of(bytes)"
);
Ok(id)
}
fn get(&self, id: &Id) -> Result<Vec<u8>> {
self.engine
.get_blob(blob_id(id))
.map_err(|e| map_engine_error("get_blob", &e))
}
fn get_range(&self, id: &Id, offset: u64, len: u64) -> Result<Vec<u8>> {
let out = self
.engine
.read_blob_range(
blob_id(id),
offset,
usize::try_from(len).unwrap_or(usize::MAX),
)
.map_err(|e| map_engine_error("read_blob_range", &e))?;
if out.len() as u64 != len {
return Err(Error::integrity_mismatch(format!(
"object {id} range [{offset}, {}) is out of bounds (engine returned {} bytes)",
offset.saturating_add(len),
out.len()
)));
}
Ok(out)
}
fn contains(&self, id: &Id) -> Result<bool> {
self.engine
.contains(blob_id(id))
.map_err(|e| map_engine_error("contains", &e))
}
fn list(&self) -> Result<Vec<(Id, u64)>> {
Err(Error::unsupported_feature(
"EntropyFS exposes no per-blob enumeration; a mark-and-sweep closure \
cannot be computed through this adapter",
))
}
fn remove(&self, _id: &Id) -> Result<u64> {
Err(Error::unsupported_feature(
"EntropyFS exposes no per-blob delete; GC cannot reclaim through this \
adapter (reclamation is EntropyFS's own reachability-GC policy)",
))
}
}
fn blob_id(id: &Id) -> BlobId {
BlobId::new(*id.as_bytes())
}
pub(crate) fn map_engine_error(context: &str, e: &EngineError) -> Error {
use entropyfs::engine::ErrorCode;
let message = format!("entropyfs {context}: {e}");
match e.code {
ErrorCode::NotFound => Error::missing_external_object(message),
ErrorCode::CorruptStore => Error::integrity_mismatch(message),
ErrorCode::InvalidArgument => Error::usage(message),
ErrorCode::ResourceLimit => Error::resource_limit(message),
ErrorCode::Unsupported => Error::unsupported_feature(message),
ErrorCode::IncompatibleFormat => Error::unsupported_version(message),
ErrorCode::Busy => Error::io(message),
ErrorCode::Internal => Error::internal_invariant(message),
ErrorCode::Closed => Error::internal_invariant(message),
_ => Error::io(message),
}
}
#[cfg(feature = "field")]
fn seed_blob_bytes(canonical: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(SEED_NODE_DOMAIN.len() + canonical.len());
out.extend_from_slice(SEED_NODE_DOMAIN);
out.extend_from_slice(canonical);
out
}
#[cfg(feature = "field")]
fn strip_seed_domain(blob: &[u8]) -> Option<&[u8]> {
blob.strip_prefix(SEED_NODE_DOMAIN)
}
#[cfg(feature = "field")]
impl EntropyFsStore {
pub(crate) fn seed_put(&self, canonical: &[u8]) -> Result<NodeId> {
let id = NodeId::of_node(canonical);
let blob = self
.engine
.put_blob(&seed_blob_bytes(canonical))
.map_err(|e| map_engine_error("put_blob", &e))?;
debug_assert_eq!(
blob.as_bytes(),
id.as_bytes(),
"engine BlobId must equal the domain-prefixed NodeId"
);
Ok(id)
}
pub(crate) fn seed_get(&self, id: &NodeId) -> Result<Vec<u8>> {
let blob = self
.engine
.get_blob(BlobId::new(*id.as_bytes()))
.map_err(|e| map_engine_error("get_blob", &e))?;
let canonical = strip_seed_domain(&blob).ok_or_else(|| {
Error::integrity_mismatch(format!("seed blob {id} is missing its domain prefix"))
})?;
let actual = NodeId::of_node(canonical);
if actual != *id {
return Err(Error::integrity_mismatch(format!(
"seed node {id} content hashes to {actual}"
)));
}
Ok(canonical.to_vec())
}
pub(crate) fn seed_get_range(&self, id: &NodeId, offset: u64, len: u64) -> Result<Vec<u8>> {
let start = offset
.checked_add(SEED_NODE_DOMAIN.len() as u64)
.ok_or_else(|| Error::integrity_mismatch("seed node range start overflows"))?;
let out = self
.engine
.read_blob_range(
BlobId::new(*id.as_bytes()),
start,
usize::try_from(len).unwrap_or(usize::MAX),
)
.map_err(|e| map_engine_error("read_blob_range", &e))?;
if out.len() as u64 != len {
return Err(Error::integrity_mismatch(format!(
"seed node {id} range [{offset}, {}) is out of bounds (engine returned {} bytes)",
offset.saturating_add(len),
out.len()
)));
}
Ok(out)
}
pub(crate) fn seed_contains(&self, id: &NodeId) -> Result<bool> {
self.engine
.contains(BlobId::new(*id.as_bytes()))
.map_err(|e| map_engine_error("contains", &e))
}
pub(crate) fn seed_list(&self) -> Result<Vec<(NodeId, u64)>> {
Err(Error::unsupported_feature(
"EntropyFS exposes no per-blob enumeration; seed nodes are one blob each \
but a mark-and-sweep closure cannot be computed through this substrate",
))
}
}
#[cfg(feature = "field")]
impl SeedStore for EntropyFsStore {
fn put_node(&mut self, canonical: &[u8]) -> Result<NodeId> {
self.seed_put(canonical)
}
fn get_node(&self, id: &NodeId) -> Result<Vec<u8>> {
self.seed_get(id)
}
fn get_node_range(&self, id: &NodeId, offset: u64, len: u64) -> Result<Vec<u8>> {
self.seed_get_range(id, offset, len)
}
fn contains_node(&self, id: &NodeId) -> Result<bool> {
self.seed_contains(id)
}
fn list_nodes(&self) -> Result<Vec<(NodeId, u64)>> {
self.seed_list()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blob_id_matches_id() {
let bytes = b"identity equivalence";
assert_eq!(
*blob_id(&Id::of(bytes)).as_bytes(),
*Id::of(bytes).as_bytes()
);
}
}
#[cfg(all(test, feature = "field"))]
mod seed_tests {
use super::*;
use crate::ErrorClass;
use std::path::PathBuf;
fn temp_root(label: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"vole-entropyfs-seed-{label}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
p
}
#[test]
fn put_get_range_roundtrip_through_the_engine() {
let root = temp_root("rt");
let mut store = EntropyFsStore::create(&root).unwrap();
let canonical = b"canonical seed node bytes";
let id1 = store.put_node(canonical).unwrap();
let id2 = store.put_node(canonical).unwrap();
assert_eq!(id1, id2, "put_node must be idempotent");
assert_eq!(store.get_node(&id1).unwrap(), canonical);
assert!(store.contains_node(&id1).unwrap());
assert_eq!(store.get_node_range(&id1, 0, 9).unwrap(), b"canonical");
let e = store.get_node_range(&id1, 100, 4).unwrap_err();
assert_eq!(e.class(), ErrorClass::IntegrityMismatch);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn node_ids_are_engine_blob_ids() {
let root = temp_root("ids");
let mut store = EntropyFsStore::create(&root).unwrap();
let canonical = b"one node one blob";
let id = store.put_node(canonical).unwrap();
assert!(
store
.engine()
.contains(BlobId::new(*id.as_bytes()))
.unwrap()
);
let m = store.engine().metrics().unwrap();
assert_eq!(
m.accounting.blob_count, 1,
"one canonical node must be one engine blob"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn list_nodes_declines_with_a_typed_error() {
let root = temp_root("list");
let mut store = EntropyFsStore::create(&root).unwrap();
store.put_node(b"a node").unwrap();
let e = store.list_nodes().unwrap_err();
assert_eq!(e.class(), ErrorClass::UnsupportedFeature);
std::fs::remove_dir_all(&root).ok();
}
}