use std::collections::HashSet;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::sync::Mutex;
use crate::compressor;
use crate::error::CasError;
use crate::hash::Hash;
use crate::hasher;
use crate::pack::{PackCache, PackFile, PackIndex};
const BLOB_CACHE_CAPACITY: usize = 1024;
#[cfg(feature = "zstd")]
#[must_use]
pub fn is_zstd_compressed(data: &[u8]) -> bool {
data.len() >= 4 && data[..4] == [0x28, 0xB5, 0x2F, 0xFD]
}
#[cfg(not(feature = "zstd"))]
#[must_use]
pub fn is_zstd_compressed(_data: &[u8]) -> bool {
false
}
pub struct BlobStore {
root: PathBuf,
compress: bool,
#[cfg_attr(not(feature = "zstd"), allow(dead_code))]
compression_level: i32,
verify_on_read: bool,
pack_cache: Mutex<Option<PackCache>>,
blob_cache: Mutex<Vec<(Hash, Vec<u8>)>>,
known_dirs: Mutex<HashSet<PathBuf>>,
}
impl BlobStore {
pub fn new(root: impl Into<PathBuf>) -> Result<Self, CasError> {
let root = root.into();
let objects_dir = root.join("objects");
fs::create_dir_all(&objects_dir)?;
Ok(Self {
root,
compress: true,
compression_level: compressor::DEFAULT_COMPRESSION_LEVEL,
verify_on_read: true,
pack_cache: Mutex::new(None),
blob_cache: Mutex::new(Vec::with_capacity(BLOB_CACHE_CAPACITY)),
known_dirs: Mutex::new(HashSet::new()),
})
}
pub fn open_in_memory() -> Result<(tempfile::TempDir, Self), CasError> {
let root = tempfile::tempdir()?;
let objects_dir = root.path().join("objects");
fs::create_dir_all(&objects_dir)?;
let store = Self {
root: root.path().to_path_buf(),
compress: true,
compression_level: compressor::DEFAULT_COMPRESSION_LEVEL,
verify_on_read: true,
pack_cache: Mutex::new(None),
blob_cache: Mutex::new(Vec::with_capacity(BLOB_CACHE_CAPACITY)),
known_dirs: Mutex::new(HashSet::new()),
};
Ok((root, store))
}
pub fn new_uncompressed(root: impl Into<PathBuf>) -> Result<Self, CasError> {
let mut store = Self::new(root)?;
store.compress = false;
Ok(store)
}
pub fn set_verify_on_read(&mut self, verify: bool) {
self.verify_on_read = verify;
}
pub fn verify_on_read(&self) -> bool {
self.verify_on_read
}
fn ensure_parent_dir(&self, parent: &std::path::Path) -> Result<(), CasError> {
{
let known = self
.known_dirs
.lock()
.map_err(|e| CasError::LockPoisoned(e.to_string()))?;
if known.contains(parent) {
return Ok(());
}
}
fs::create_dir_all(parent)?;
self.known_dirs
.lock()
.map_err(|e| CasError::LockPoisoned(e.to_string()))?
.insert(parent.to_path_buf());
Ok(())
}
pub fn put_blob(&self, data: &[u8]) -> Result<Hash, CasError> {
let hash = hasher::hash_bytes(data);
let blob_path = self.blob_path(&hash);
if blob_path.exists() {
return Ok(hash);
}
if let Some(parent) = blob_path.parent() {
self.ensure_parent_dir(parent)?;
}
#[cfg(feature = "zstd")]
if self.compress {
let compressed = compressor::compress(data, self.compression_level)?;
fs::write(&blob_path, &compressed)?;
} else {
fs::write(&blob_path, data)?;
}
#[cfg(not(feature = "zstd"))]
fs::write(&blob_path, data)?;
Ok(hash)
}
pub fn put_blob_new(&self, data: &[u8]) -> Result<Hash, CasError> {
let hash = hasher::hash_bytes(data);
let blob_path = self.blob_path(&hash);
if blob_path.exists() {
return Err(CasError::AlreadyExists(hash.to_hex()));
}
if let Some(parent) = blob_path.parent() {
self.ensure_parent_dir(parent)?;
}
#[cfg(feature = "zstd")]
if self.compress {
let compressed = compressor::compress(data, self.compression_level)?;
fs::write(&blob_path, &compressed)?;
} else {
fs::write(&blob_path, data)?;
}
#[cfg(not(feature = "zstd"))]
fs::write(&blob_path, data)?;
Ok(hash)
}
pub fn put_blob_with_hash(&self, data: &[u8], expected_hash: &Hash) -> Result<(), CasError> {
let blob_path = self.blob_path(expected_hash);
if blob_path.exists() {
return Ok(());
}
hasher::verify_hash(data, expected_hash)?;
if let Some(parent) = blob_path.parent() {
self.ensure_parent_dir(parent)?;
}
#[cfg(feature = "zstd")]
if self.compress {
let compressed = compressor::compress(data, self.compression_level)?;
fs::write(&blob_path, &compressed)?;
} else {
fs::write(&blob_path, data)?;
}
#[cfg(not(feature = "zstd"))]
fs::write(&blob_path, data)?;
Ok(())
}
pub fn get_blob(&self, hash: &Hash) -> Result<Vec<u8>, CasError> {
{
let mut cache = self
.blob_cache
.lock()
.map_err(|e| CasError::LockPoisoned(e.to_string()))?;
if let Some(pos) = cache.iter().position(|(h, _)| h == hash) {
let (_, data) = cache.remove(pos);
cache.insert(0, (*hash, data.clone()));
return Ok(data);
}
}
let data = if self.blob_path(hash).exists() {
let raw = fs::read(self.blob_path(hash))?;
#[cfg(feature = "zstd")]
let result = if is_zstd_compressed(&raw) {
compressor::decompress(&raw)?
} else {
raw
};
#[cfg(not(feature = "zstd"))]
let result = raw;
if self.verify_on_read {
hasher::verify_hash(&result, hash)?;
}
result
} else {
match self.get_blob_packed(hash) {
Ok(data) => data,
Err(CasError::BlobNotFound(_)) => {
return Err(CasError::BlobNotFound(hash.to_hex()))
}
Err(e) => return Err(e),
}
};
self.cache_blob(*hash, data.clone());
Ok(data)
}
fn cache_blob(&self, hash: Hash, data: Vec<u8>) {
let Ok(mut cache) = self.blob_cache.lock() else {
return;
};
if cache.len() >= BLOB_CACHE_CAPACITY {
cache.pop();
}
cache.insert(0, (hash, data));
}
pub fn has_blob(&self, hash: &Hash) -> bool {
self.blob_path(hash).exists() || self.has_blob_packed(hash)
}
pub fn delete_blob(&self, hash: &Hash) -> Result<(), CasError> {
let blob_path = self.blob_path(hash);
fs::remove_file(&blob_path).map_err(|e| {
if e.kind() == io::ErrorKind::NotFound {
CasError::BlobNotFound(hash.to_hex())
} else {
CasError::Io(e)
}
})
}
pub fn blob_count(&self) -> Result<u64, CasError> {
let objects_dir = self.root.join("objects");
let mut count = 0u64;
if objects_dir.exists() {
for entry in fs::read_dir(&objects_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let dir_name = entry.file_name();
if dir_name == "pack" {
continue;
}
for sub_entry in fs::read_dir(entry.path())? {
let sub_entry = sub_entry?;
if sub_entry.file_type()?.is_file() {
count += 1;
}
}
}
}
}
Ok(count)
}
pub fn total_size(&self) -> Result<u64, CasError> {
let objects_dir = self.root.join("objects");
let mut total = 0u64;
if objects_dir.exists() {
for entry in fs::read_dir(&objects_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let dir_name = entry.file_name();
if dir_name == "pack" {
continue;
}
for sub_entry in fs::read_dir(entry.path())? {
let sub_entry = sub_entry?;
if sub_entry.file_type()?.is_file() {
total += sub_entry.metadata()?.len();
}
}
}
}
}
Ok(total)
}
pub fn list_blobs(&self) -> Result<Vec<Hash>, CasError> {
let objects_dir = self.root.join("objects");
let mut hashes = Vec::new();
if !objects_dir.exists() {
return Ok(hashes);
}
for entry in fs::read_dir(&objects_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let dir_name = entry.file_name();
if dir_name == "pack" {
continue;
}
let prefix = dir_name.to_string_lossy().to_string();
for sub_entry in fs::read_dir(entry.path())? {
let sub_entry = sub_entry?;
if sub_entry.file_type()?.is_file() {
let suffix = sub_entry.file_name().to_string_lossy().to_string();
let hex = format!("{prefix}{suffix}");
if let Ok(hash) = Hash::from_hex(&hex) {
hashes.push(hash);
}
}
}
}
}
hashes.sort();
Ok(hashes)
}
pub fn objects_dir(&self) -> PathBuf {
self.root.join("objects")
}
#[must_use]
pub fn root(&self) -> &std::path::Path {
&self.root
}
pub fn pack_dir(&self) -> PathBuf {
self.root.join("objects").join("pack")
}
fn with_pack_cache<F, R>(&self, f: F) -> Result<R, CasError>
where
F: FnOnce(&PackCache) -> R,
{
let mut guard = self
.pack_cache
.lock()
.map_err(|e| CasError::LockPoisoned(e.to_string()))?;
if guard.is_none() {
let cache = PackCache::load_all(&self.pack_dir())?;
*guard = Some(cache);
}
#[allow(clippy::expect_used)]
let cache = guard
.as_ref()
.expect("pack cache was populated two statements above under an exclusive lock");
Ok(f(cache))
}
pub fn invalidate_pack_cache(&self) {
if let Ok(mut guard) = self.pack_cache.lock() {
*guard = None;
}
}
pub fn get_blob_packed(&self, hash: &Hash) -> Result<Vec<u8>, CasError> {
let pack_path = self.with_pack_cache(|cache| cache.find(hash).map(|(p, _)| p.clone()))?;
let pack_path = pack_path.ok_or_else(|| CasError::BlobNotFound(hash.to_hex()))?;
let idx_path = pack_path.with_extension("idx");
let index = PackIndex::load(&idx_path)?;
let data = PackFile::read_blob(&pack_path, &index, hash)?;
Ok(data)
}
pub fn has_blob_packed(&self, hash: &Hash) -> bool {
self.with_pack_cache(|cache| cache.find(hash).is_some())
.unwrap_or(false)
}
pub fn list_blobs_packed(&self) -> Result<Vec<Hash>, CasError> {
self.with_pack_cache(PackCache::all_hashes)
}
pub fn repack(&self, threshold: usize) -> Result<usize, CasError> {
let loose_hashes = self.list_blobs()?;
if loose_hashes.len() <= threshold {
return Ok(0);
}
let mut objects = Vec::with_capacity(loose_hashes.len());
for hash in &loose_hashes {
let data = self.get_blob(hash)?;
objects.push((*hash, data));
}
let (pack_path, _idx_path) = PackFile::create(&self.pack_dir(), &objects)?;
debug_assert!(pack_path.exists());
for hash in &loose_hashes {
let _ = self.delete_blob(hash);
}
self.invalidate_pack_cache();
Ok(loose_hashes.len())
}
fn blob_path(&self, hash: &Hash) -> PathBuf {
let hex = hash.to_hex();
let prefix = &hex[..2];
let suffix = &hex[2..];
self.root.join("objects").join(prefix).join(suffix)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn make_store() -> Result<(TempDir, BlobStore), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
Ok((dir, store))
}
fn pack_cache_loaded(store: &BlobStore) -> Result<bool, Box<dyn std::error::Error>> {
Ok(store
.pack_cache
.lock()
.map_err(|_| "pack cache lock poisoned")?
.is_some())
}
#[test]
fn test_put_and_get_blob() -> TestResult {
let (_dir, store) = make_store()?;
let data = b"hello, suture!";
let hash = store.put_blob(data)?;
let retrieved = store.get_blob(&hash)?;
assert_eq!(data.as_slice(), retrieved.as_slice());
Ok(())
}
#[test]
fn test_deduplication() -> TestResult {
let (_dir, store) = make_store()?;
let data = b"deduplicate me";
let h1 = store.put_blob(data)?;
let h2 = store.put_blob(data)?;
assert_eq!(h1, h2);
assert_eq!(store.blob_count()?, 1, "Only one copy should exist");
Ok(())
}
#[test]
fn test_has_blob() -> TestResult {
let (_dir, store) = make_store()?;
let hash = store.put_blob(b"exists")?;
assert!(store.has_blob(&hash));
let missing = Hash::from_hex(&"f".repeat(64))?;
assert!(!store.has_blob(&missing));
Ok(())
}
#[test]
fn test_get_nonexistent_blob() -> TestResult {
let (_dir, store) = make_store()?;
let missing = Hash::from_hex(&"a".repeat(64))?;
let result = store.get_blob(&missing);
assert!(matches!(result, Err(CasError::BlobNotFound(_))));
Ok(())
}
#[test]
fn test_delete_blob() -> TestResult {
let (_dir, store) = make_store()?;
let hash = store.put_blob(b"delete me")?;
assert!(store.has_blob(&hash));
store.delete_blob(&hash)?;
assert!(!store.has_blob(&hash));
Ok(())
}
#[test]
fn test_delete_nonexistent_blob() -> TestResult {
let (_dir, store) = make_store()?;
let missing = Hash::from_hex(&"b".repeat(64))?;
let result = store.delete_blob(&missing);
assert!(matches!(result, Err(CasError::BlobNotFound(_))));
Ok(())
}
#[test]
fn test_put_blob_new_rejects_duplicate() -> TestResult {
let (_dir, store) = make_store()?;
let data = b"duplicate";
store.put_blob(data)?;
let result = store.put_blob_new(data);
assert!(matches!(result, Err(CasError::AlreadyExists(_))));
Ok(())
}
#[test]
fn test_put_blob_with_hash_verifies() -> TestResult {
let (_dir, store) = make_store()?;
let data = b"verified content";
let hash = hasher::hash_bytes(data);
store.put_blob_with_hash(data, &hash)?;
let wrong = hasher::hash_bytes(b"other content");
let result = store.put_blob_with_hash(b"original", &wrong);
assert!(matches!(result, Err(CasError::HashMismatch { .. })));
Ok(())
}
#[test]
fn test_blob_count_and_list() -> TestResult {
let (_dir, store) = make_store()?;
store.put_blob(b"one")?;
store.put_blob(b"two")?;
store.put_blob(b"three")?;
assert_eq!(store.blob_count()?, 3);
assert_eq!(store.list_blobs()?.len(), 3);
Ok(())
}
#[test]
fn test_large_blob() -> TestResult {
let (_dir, store) = make_store()?;
let data: Vec<u8> = (0..10_000_000).map(|i| (i % 256) as u8).collect();
let hash = store.put_blob(&data)?;
let retrieved = store.get_blob(&hash)?;
assert_eq!(data.len(), retrieved.len());
assert_eq!(data, retrieved);
Ok(())
}
#[test]
fn test_hash_integrity() -> TestResult {
let (_dir, store) = make_store()?;
let data = b"integrity check";
let hash = store.put_blob(data)?;
let blob_path = store.blob_path(&hash);
let mut corrupted = fs::read(&blob_path)?;
corrupted[0] = corrupted[0].wrapping_add(1);
fs::write(&blob_path, &corrupted)?;
let result = store.get_blob(&hash);
assert!(matches!(result, Err(CasError::HashMismatch { .. })));
Ok(())
}
#[test]
fn test_verify_on_read_disabled_skips_check() -> TestResult {
let (_dir, store) = make_store()?;
let data = b"trust me";
let hash = store.put_blob(data)?;
let mut store = store;
store.set_verify_on_read(false);
assert!(!store.verify_on_read());
let blob_path = store.blob_path(&hash);
let mut corrupted = fs::read(&blob_path)?;
corrupted[0] = corrupted[0].wrapping_add(1);
fs::write(&blob_path, &corrupted)?;
let result = store.get_blob(&hash)?;
assert_eq!(result, corrupted);
Ok(())
}
#[cfg(feature = "zstd")]
#[test]
fn test_compressed_store() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new(dir.path())?;
let data = b"this will be compressed";
let hash = store.put_blob(data)?;
let blob_path = store.blob_path(&hash);
let raw = fs::read(&blob_path)?;
assert!(is_zstd_compressed(&raw), "Blob should be Zstd-compressed");
let retrieved = store.get_blob(&hash)?;
assert_eq!(data.as_slice(), retrieved.as_slice());
Ok(())
}
#[cfg(feature = "zstd")]
#[test]
fn test_decompress_rejects_zip_bomb() -> TestResult {
use std::io::Write;
let dir = tempfile::tempdir()?;
let store = BlobStore::new(dir.path())?;
let mut encoder = zstd::Encoder::new(Vec::new(), 3)?;
let chunk = [0u8; 65536];
let total = compressor::MAX_DECOMPRESSED_SIZE + 1;
let mut written = 0usize;
while written < total {
encoder.write_all(&chunk)?;
written += chunk.len();
}
let frame = encoder.finish()?;
let addr = Hash::from_hex(&"7".repeat(64))?;
let path = store.blob_path(&addr);
fs::create_dir_all(path.parent().ok_or("address path has no parent")?)?;
fs::write(&path, &frame)?;
let result = store.get_blob(&addr);
assert!(
matches!(result, Err(CasError::DecompressionTooLarge { .. })),
"zip bomb must be rejected"
);
Ok(())
}
#[test]
fn test_blob_path_layout() -> TestResult {
let (_dir, store) = make_store()?;
let hash = hasher::hash_bytes(b"layout");
let path = store.blob_path(&hash);
let expected = store
.objects_dir()
.join(&hash.to_hex()[..2])
.join(&hash.to_hex()[2..]);
assert_eq!(path, expected);
Ok(())
}
#[test]
fn test_in_memory_store() -> TestResult {
let (_root, store) = BlobStore::open_in_memory()?;
let hash = store.put_blob(b"in-memory")?;
assert_eq!(store.get_blob(&hash)?, b"in-memory".to_vec());
Ok(())
}
mod proptests {
use super::*;
use proptest::prelude::*;
fn soft<T, E: std::fmt::Display>(r: Result<T, E>) -> Result<T, TestCaseError> {
r.map_err(|e| TestCaseError::fail(e.to_string()))
}
fn arb_bytes(max: usize) -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(proptest::num::u8::ANY, 0..max)
}
proptest! {
#[test]
fn put_get_roundtrip(data in arb_bytes(1024)) {
let dir = soft(tempfile::tempdir())?;
let store = soft(BlobStore::new_uncompressed(dir.path()))?;
let hash = soft(store.put_blob(&data))?;
let retrieved = soft(store.get_blob(&hash))?;
prop_assert_eq!(data, retrieved);
}
#[test]
fn content_addressing(data1 in arb_bytes(512), data2 in arb_bytes(512)) {
let dir = soft(tempfile::tempdir())?;
let store = soft(BlobStore::new_uncompressed(dir.path()))?;
let hash1 = soft(store.put_blob(&data1))?;
let hash2 = soft(store.put_blob(&data2))?;
if data1 == data2 {
prop_assert_eq!(hash1, hash2, "same data must produce same hash");
} else {
prop_assert_ne!(hash1, hash2, "different data must produce different hashes");
}
}
#[test]
fn put_twice_idempotent(data in arb_bytes(1024)) {
let dir = soft(tempfile::tempdir())?;
let store = soft(BlobStore::new_uncompressed(dir.path()))?;
let hash1 = soft(store.put_blob(&data))?;
let hash2 = soft(store.put_blob(&data))?;
prop_assert_eq!(hash1, hash2);
prop_assert_eq!(soft(store.blob_count())?, 1);
}
}
}
mod pack_tests {
use super::*;
#[test]
fn test_get_blob_from_pack() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
let hash1 = store.put_blob(b"packed blob one")?;
let hash2 = store.put_blob(b"packed blob two")?;
let packed = store.repack(0)?;
assert_eq!(packed, 2);
assert_eq!(store.blob_count()?, 0);
let data1 = store.get_blob(&hash1)?;
assert_eq!(data1, b"packed blob one".to_vec());
let data2 = store.get_blob(&hash2)?;
assert_eq!(data2, b"packed blob two".to_vec());
Ok(())
}
#[test]
fn test_has_blob_checks_packs() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
let hash = store.put_blob(b"check me in packs")?;
store.repack(0)?;
assert!(store.has_blob(&hash));
let missing = Hash::from_hex(&"c".repeat(64))?;
assert!(!store.has_blob(&missing));
Ok(())
}
#[test]
fn test_get_blob_packed_not_found() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
let missing = Hash::from_hex(&"d".repeat(64))?;
let result = store.get_blob_packed(&missing);
assert!(matches!(result, Err(CasError::BlobNotFound(_))));
Ok(())
}
#[test]
fn test_list_blobs_packed() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
store.put_blob(b"alpha")?;
store.put_blob(b"beta")?;
store.repack(0)?;
let packed = store.list_blobs_packed()?;
assert_eq!(packed.len(), 2);
Ok(())
}
#[test]
fn test_repack_below_threshold() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
store.put_blob(b"only one")?;
let packed = store.repack(10)?;
assert_eq!(packed, 0);
assert_eq!(store.blob_count()?, 1);
Ok(())
}
#[test]
fn test_repack_at_threshold() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
store.put_blob(b"one")?;
store.put_blob(b"two")?;
let packed = store.repack(2)?;
assert_eq!(packed, 0);
assert_eq!(store.blob_count()?, 2);
let packed = store.repack(1)?;
assert_eq!(packed, 2);
assert_eq!(store.blob_count()?, 0);
Ok(())
}
#[test]
fn test_loose_priority_over_packed() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
let hash = store.put_blob(b"original data")?;
store.repack(0)?;
let blob_path = store.blob_path(&hash);
if let Some(parent) = blob_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&blob_path, b"original data")?;
let data = store.get_blob(&hash)?;
assert_eq!(data, b"original data".to_vec());
store.delete_blob(&hash)?;
let data = store.get_blob(&hash)?;
assert_eq!(data, b"original data".to_vec());
Ok(())
}
#[test]
fn test_has_blob_packed() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
let hash = store.put_blob(b"packed check")?;
assert!(!store.has_blob_packed(&hash));
store.repack(0)?;
assert!(store.has_blob_packed(&hash));
Ok(())
}
#[test]
fn test_repack_multiple_times() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
store.put_blob(b"first batch one")?;
store.put_blob(b"first batch two")?;
store.repack(0)?;
store.put_blob(b"second batch")?;
store.repack(0)?;
let all = store.list_blobs_packed()?;
assert_eq!(all.len(), 3);
Ok(())
}
#[test]
fn test_pack_cache_avoids_repeated_disk_reads() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
let hash = store.put_blob(b"cache me")?;
store.repack(0)?;
assert!(store.has_blob_packed(&hash));
assert!(
pack_cache_loaded(&store)?,
"pack cache should be populated after first access"
);
assert!(store.has_blob_packed(&hash));
let data = store.get_blob_packed(&hash)?;
assert_eq!(data, b"cache me".to_vec());
Ok(())
}
#[test]
fn test_invalidate_pack_cache() -> TestResult {
let dir = tempfile::tempdir()?;
let store = BlobStore::new_uncompressed(dir.path())?;
let hash = store.put_blob(b"invalidate test")?;
store.repack(0)?;
assert!(store.has_blob_packed(&hash));
assert!(pack_cache_loaded(&store)?);
store.invalidate_pack_cache();
assert!(!pack_cache_loaded(&store)?);
assert!(store.has_blob_packed(&hash));
assert!(pack_cache_loaded(&store)?);
Ok(())
}
}
}