use std::path::Path;
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
use super::key::CacheKey;
use super::serialize::CachedBuild;
const CACHE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("cache");
const METADATA_TABLE: TableDefinition<&str, &str> = TableDefinition::new("metadata");
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error("cache miss")]
CacheMiss,
#[error("cache database error: {0}")]
DatabaseError(String),
#[error("serialization error: {0}")]
SerializationError(String),
#[error("deserialization error: {0}")]
DeserializationError(String),
#[error("io error: {0}")]
IoError(#[from] std::io::Error),
#[error("cache version mismatch: expected {expected}, found {found}")]
VersionMismatch { expected: u32, found: u32 },
#[error("cache corrupted: {0}")]
Corrupted(String),
#[error("invalid data: {0}")]
InvalidData(String),
}
impl From<redb::Error> for CacheError {
fn from(err: redb::Error) -> Self {
CacheError::DatabaseError(err.to_string())
}
}
impl From<redb::DatabaseError> for CacheError {
fn from(err: redb::DatabaseError) -> Self {
CacheError::DatabaseError(err.to_string())
}
}
impl From<redb::TableError> for CacheError {
fn from(err: redb::TableError) -> Self {
CacheError::DatabaseError(err.to_string())
}
}
impl From<redb::TransactionError> for CacheError {
fn from(err: redb::TransactionError) -> Self {
CacheError::DatabaseError(err.to_string())
}
}
impl From<redb::StorageError> for CacheError {
fn from(err: redb::StorageError) -> Self {
CacheError::DatabaseError(err.to_string())
}
}
impl From<redb::CommitError> for CacheError {
fn from(err: redb::CommitError) -> Self {
CacheError::DatabaseError(err.to_string())
}
}
pub struct CacheStore {
db: Database,
}
impl CacheStore {
pub fn open(cache_dir: &Path) -> Result<Self, CacheError> {
std::fs::create_dir_all(cache_dir)?;
let db_path = cache_dir.join("cache.redb");
let db = Database::create(&db_path)?;
let write_txn = db.begin_write()?;
{
let _ = write_txn.open_table(CACHE_TABLE)?;
let _ = write_txn.open_table(METADATA_TABLE)?;
}
write_txn.commit()?;
Ok(Self { db })
}
pub fn get(&self, key: &CacheKey) -> Result<CachedBuild, CacheError> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(CACHE_TABLE)?;
let value = table.get(key.as_hex())?.ok_or(CacheError::CacheMiss)?;
let bytes = value.value();
let cached: CachedBuild = bincode::deserialize(bytes)
.map_err(|e| CacheError::DeserializationError(e.to_string()))?;
if !cached.metadata.is_compatible() {
return Err(CacheError::VersionMismatch {
expected: super::serialize::CACHE_FORMAT_VERSION,
found: cached.metadata.format_version,
});
}
Ok(cached)
}
pub fn put(&self, key: &CacheKey, build: &CachedBuild) -> Result<(), CacheError> {
let bytes =
bincode::serialize(build).map_err(|e| CacheError::SerializationError(e.to_string()))?;
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(CACHE_TABLE)?;
table.insert(key.as_hex(), bytes.as_slice())?;
}
write_txn.commit()?;
Ok(())
}
pub fn remove(&self, key: &CacheKey) -> Result<(), CacheError> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(CACHE_TABLE)?;
table.remove(key.as_hex())?;
}
write_txn.commit()?;
Ok(())
}
pub fn contains(&self, key: &CacheKey) -> Result<bool, CacheError> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(CACHE_TABLE)?;
Ok(table.get(key.as_hex())?.is_some())
}
pub fn clear(&self) -> Result<(), CacheError> {
let write_txn = self.db.begin_write()?;
{
write_txn.delete_table(CACHE_TABLE)?;
let _ = write_txn.open_table(CACHE_TABLE)?;
}
write_txn.commit()?;
Ok(())
}
pub fn len(&self) -> Result<usize, CacheError> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(CACHE_TABLE)?;
let count = table.iter()?.count();
Ok(count)
}
pub fn is_empty(&self) -> Result<bool, CacheError> {
Ok(self.len()? == 0)
}
pub fn set_metadata(&self, key: &str, value: &str) -> Result<(), CacheError> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_TABLE)?;
table.insert(key, value)?;
}
write_txn.commit()?;
Ok(())
}
pub fn get_metadata(&self, key: &str) -> Result<Option<String>, CacheError> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_TABLE)?;
Ok(table.get(key)?.map(|v| v.value().to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_store() -> (CacheStore, TempDir) {
let dir = TempDir::new().unwrap();
let store = CacheStore::open(dir.path()).unwrap();
(store, dir)
}
#[test]
fn test_store_open_creates_directory() {
let dir = TempDir::new().unwrap();
let cache_dir = dir.path().join("new_cache");
let _store = CacheStore::open(&cache_dir).unwrap();
assert!(cache_dir.exists());
assert!(cache_dir.join("cache.redb").exists());
}
#[test]
fn test_cache_miss() {
let (store, _dir) = create_test_store();
let key = CacheKey::from_hex("nonexistent");
let result = store.get(&key);
assert!(matches!(result, Err(CacheError::CacheMiss)));
}
#[test]
fn test_contains() {
let (store, _dir) = create_test_store();
let key = CacheKey::from_hex("test_key");
assert!(!store.contains(&key).unwrap());
}
#[test]
fn test_metadata() {
let (store, _dir) = create_test_store();
store.set_metadata("version", "1.0").unwrap();
let value = store.get_metadata("version").unwrap();
assert_eq!(value, Some("1.0".to_string()));
}
#[test]
fn test_clear() {
let (store, _dir) = create_test_store();
assert!(store.is_empty().unwrap());
store.clear().unwrap();
assert!(store.is_empty().unwrap());
}
}