use celln_manifest::Hash;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("object not found: {0}")]
NotFound(Hash),
#[error("integrity failure for {expected}: bytes on disk hash to {actual}")]
Integrity { expected: Hash, actual: Hash },
#[error("hash {0} is not in the expected blake3:<hex> form")]
BadHash(String),
}
pub struct Store {
objects: PathBuf,
}
impl Store {
pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
let objects = root.as_ref().join("objects");
fs::create_dir_all(&objects)?;
Ok(Store { objects })
}
fn hex_of(hash: &Hash) -> Result<&str, StoreError> {
hash.0
.strip_prefix("blake3:")
.ok_or_else(|| StoreError::BadHash(hash.0.clone()))
}
fn path_for(&self, hash: &Hash) -> Result<PathBuf, StoreError> {
let hex = Self::hex_of(hash)?;
let (fanout, _) = hex.split_at(2.min(hex.len()));
Ok(self.objects.join(fanout).join(hex))
}
pub fn put(&self, bytes: &[u8]) -> Result<Hash, StoreError> {
let hash = Hash::of(bytes);
let path = self.path_for(&hash)?;
if path.exists() {
return Ok(hash); }
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let tmp = path.with_extension("tmp");
fs::write(&tmp, bytes)?;
fs::rename(&tmp, &path)?;
Ok(hash)
}
pub fn get(&self, hash: &Hash) -> Result<Vec<u8>, StoreError> {
let path = self.path_for(hash)?;
if !path.exists() {
return Err(StoreError::NotFound(hash.clone()));
}
let bytes = fs::read(&path)?;
let actual = Hash::of(&bytes);
if &actual != hash {
return Err(StoreError::Integrity {
expected: hash.clone(),
actual,
});
}
Ok(bytes)
}
pub fn has(&self, hash: &Hash) -> bool {
self.path_for(hash).map(|p| p.exists()).unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn put_get_roundtrip() {
let dir = tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let h = store.put(b"print(1+1)").unwrap();
assert!(store.has(&h));
assert_eq!(store.get(&h).unwrap(), b"print(1+1)");
}
#[test]
fn put_dedups() {
let dir = tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let a = store.put(b"same").unwrap();
let b = store.put(b"same").unwrap();
assert_eq!(a, b); }
#[test]
fn missing_object_is_not_found() {
let dir = tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let ghost = Hash::of(b"never stored");
assert!(matches!(store.get(&ghost), Err(StoreError::NotFound(_))));
}
#[test]
fn corruption_is_caught_on_read() {
let dir = tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let h = store.put(b"trusted tool bytes").unwrap();
let path = store.path_for(&h).unwrap();
fs::write(&path, b"trojaned bytes").unwrap();
assert!(matches!(store.get(&h), Err(StoreError::Integrity { .. })));
}
}