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};
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())
}
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(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()
);
}
}