use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use super::key::CacheKey;
pub const ROW_MAGIC: &[u8; 9] = b"LIDX-ECR1";
pub const ROW_VERSION: u8 = 1;
const ROW_PAD_LEN: usize = 2;
pub const ROW_HEADER_LEN: usize = 9 + 1 + ROW_PAD_LEN + 32 + 4 + 32;
const REFS_FILENAME: &str = "refs.json";
const TELEMETRY_FILENAME: &str = "telemetry.json";
const MODEL_INDEX_FILENAME: &str = "model_index.json";
pub const DEBUG_ENV_VAR: &str = "LEINDEX_EMBED_CACHE_DEBUG";
#[derive(Debug)]
pub struct ProbeResult {
pub hits: HashMap<usize, Vec<f32>>,
pub misses: Vec<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheCompactionReport {
pub reclaimed_bytes: u64,
pub rows_removed: u64,
pub rows_retained: u64,
pub staging_files_removed: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheTelemetry {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub entry_size_rejections: u64,
pub bytes_rejected: u64,
pub bytes_evicted: u64,
}
impl CacheTelemetry {
pub fn record_hit(&mut self) {
self.hits += 1;
}
pub fn record_miss(&mut self) {
self.misses += 1;
}
pub fn record_eviction(&mut self, bytes: u64) {
self.evictions += 1;
self.bytes_evicted += bytes;
}
pub fn record_entry_rejection(&mut self, bytes: u64) {
self.entry_size_rejections += 1;
self.bytes_rejected += bytes;
}
pub fn hit_ratio(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub max_bytes: u64,
pub max_entry_bytes: u64,
#[serde(default)]
pub debug_mode: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_bytes: 1024 * 1024 * 1024,
max_entry_bytes: 1024 * 1024,
debug_mode: false,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheStatsReport {
pub cache_bytes: u64,
pub row_count: usize,
pub telemetry: CacheTelemetry,
pub hit_ratio: f64,
pub max_bytes: u64,
pub max_entry_bytes: u64,
pub tracked_references: usize,
pub model_identity: String,
}
impl std::fmt::Display for CacheStatsReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, " Embedding Cache:")?;
writeln!(f, " cache bytes: {}", self.cache_bytes)?;
writeln!(f, " row count: {}", self.row_count)?;
writeln!(
f,
" hit ratio: {:.4} ({}/{})",
self.hit_ratio,
self.telemetry.hits,
self.telemetry.hits + self.telemetry.misses
)?;
writeln!(f, " hits: {}", self.telemetry.hits)?;
writeln!(f, " misses: {}", self.telemetry.misses)?;
writeln!(f, " evictions: {}", self.telemetry.evictions)?;
writeln!(f, " bytes evicted: {}", self.telemetry.bytes_evicted)?;
writeln!(
f,
" entry rejections: {} ({} bytes)",
self.telemetry.entry_size_rejections, self.telemetry.bytes_rejected
)?;
writeln!(f, " max bytes: {}", self.max_bytes)?;
writeln!(f, " max entry bytes: {}", self.max_entry_bytes)?;
writeln!(f, " tracked refs: {}", self.tracked_references)?;
writeln!(f, " model identity: {}", self.model_identity)?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("row validation failed: {0}")]
BadRow(String),
#[error("serde error: {0}")]
Serde(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProjectRefs {
#[serde(default)]
refs: HashMap<String, HashSet<String>>,
}
impl ProjectRefs {
pub fn add(&mut self, fingerprint: &[u8; 32], project_id: &str, generation: u64) {
let key = ref_key(project_id, generation);
let hex = hex_encode(fingerprint);
self.refs.entry(hex).or_default().insert(key);
}
pub fn remove(&mut self, fingerprint: &[u8; 32], project_id: &str, generation: u64) {
let key = ref_key(project_id, generation);
let hex = hex_encode(fingerprint);
if let Some(set) = self.refs.get_mut(&hex) {
set.remove(&key);
if set.is_empty() {
self.refs.remove(&hex);
}
}
}
pub fn is_referenced(&self, fingerprint: &[u8; 32]) -> bool {
let hex = hex_encode(fingerprint);
self.refs.get(&hex).is_some_and(|set| !set.is_empty())
}
pub fn tracked_count(&self) -> usize {
self.refs.len()
}
pub fn tracked_fingerprints(&self) -> Vec<String> {
self.refs.keys().cloned().collect()
}
}
fn ref_key(project_id: &str, generation: u64) -> String {
format!("{project_id}:{generation}")
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ModelIndex {
#[serde(default)]
map: HashMap<String, String>,
}
impl ModelIndex {
pub fn insert(&mut self, fingerprint_hex: &str, model_digest_hex: &str) {
self.map
.insert(fingerprint_hex.to_string(), model_digest_hex.to_string());
}
pub fn get(&self, fingerprint_hex: &str) -> Option<&str> {
self.map.get(fingerprint_hex).map(String::as_str)
}
pub fn remove(&mut self, fingerprint_hex: &str) {
self.map.remove(fingerprint_hex);
}
pub fn retain_existing(&mut self, existing: &HashSet<String>) {
self.map.retain(|k, _| existing.contains(k));
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
#[derive(Clone)]
struct EvictionCandidate {
path: PathBuf,
mtime: SystemTime,
size: u64,
}
pub struct GlobalEmbeddingCache {
root: PathBuf,
refs: ProjectRefs,
config: CacheConfig,
telemetry: CacheTelemetry,
model_index: ModelIndex,
}
impl GlobalEmbeddingCache {
pub fn open(root: impl AsRef<Path>) -> Result<Self, CacheError> {
Self::open_with_config(root, CacheConfig::default())
}
pub fn open_with_config(
root: impl AsRef<Path>,
config: CacheConfig,
) -> Result<Self, CacheError> {
let root = root.as_ref().to_path_buf();
fs::create_dir_all(&root)?;
fs::create_dir_all(root.join("rows"))?;
let refs = load_refs(&root)?;
let telemetry = load_telemetry(&root)?;
let model_index = load_model_index(&root)?;
Ok(GlobalEmbeddingCache {
root,
refs,
config,
telemetry,
model_index,
})
}
pub fn root(&self) -> &Path {
&self.root
}
fn prefix_dir(&self, fingerprint: &[u8; 32]) -> PathBuf {
let hex = hex_encode(fingerprint);
self.root.join("rows").join(&hex[0..2])
}
pub fn row_path(&self, fingerprint: &[u8; 32]) -> PathBuf {
let hex = hex_encode(fingerprint);
self.prefix_dir(fingerprint).join(&hex)
}
pub fn exists(&self, key: &CacheKey) -> bool {
self.row_path(&key.fingerprint()).exists()
}
pub fn probe(&mut self, keys: &[CacheKey]) -> Result<ProbeResult, CacheError> {
let mut hits = HashMap::new();
let mut misses = Vec::new();
for (i, key) in keys.iter().enumerate() {
let fingerprint = key.fingerprint();
let path = self.row_path(&fingerprint);
if !path.exists() {
self.telemetry.record_miss();
misses.push(i);
continue;
}
match read_row(&path, &fingerprint) {
Ok(vector) => {
self.telemetry.record_hit();
hits.insert(i, vector);
}
Err(_) => {
self.telemetry.record_miss();
misses.push(i);
}
}
}
Ok(ProbeResult { hits, misses })
}
pub fn put(
&mut self,
key: &CacheKey,
vector: &[f32],
source_text: Option<&str>,
) -> Result<(), CacheError> {
let fingerprint = key.fingerprint();
let final_path = self.row_path(&fingerprint);
if final_path.exists() {
return Ok(());
}
let dim = key.output_dimensions as usize;
let debug_active = is_debug_escape_hatch_active(self.config.debug_mode);
let debug_payload = if debug_active { source_text } else { None };
let row_bytes = encode_row(&fingerprint, dim, vector, debug_payload);
let entry_size = row_bytes.len() as u64;
if entry_size > self.config.max_entry_bytes {
self.telemetry.record_entry_rejection(entry_size);
return Ok(()); }
self.enforce_byte_budget(entry_size)?;
self.publish_row(&final_path, &row_bytes)?;
let fingerprint_hex = hex_encode(&fingerprint);
let model_digest_hex = hex_encode(&key.model_digest);
self.model_index.insert(&fingerprint_hex, &model_digest_hex);
let _ = self.persist_model_index();
Ok(())
}
fn enforce_byte_budget(&mut self, entry_size: u64) -> Result<(), CacheError> {
if self.config.max_bytes == 0 {
return Ok(());
}
let current = self.total_bytes()?;
if current + entry_size > self.config.max_bytes {
self.evict_unreferenced(current + entry_size - self.config.max_bytes)?;
}
Ok(())
}
fn publish_row(&self, final_path: &Path, row_bytes: &[u8]) -> Result<(), CacheError> {
if let Some(parent) = final_path.parent() {
fs::create_dir_all(parent)?;
}
let staging_path = staging_path_for(final_path);
{
let file = fs::File::create(&staging_path)?;
let mut writer = BufWriter::new(file);
writer.write_all(row_bytes)?;
writer.flush()?;
writer.get_ref().sync_all()?;
}
match fs::rename(&staging_path, final_path) {
Ok(()) => {}
Err(_) if final_path.exists() => {
let _ = fs::remove_file(&staging_path);
}
Err(e) => {
let _ = fs::remove_file(&staging_path);
return Err(CacheError::Io(e));
}
}
Ok(())
}
pub fn get(&self, key: &CacheKey) -> Result<Option<Vec<f32>>, CacheError> {
let fingerprint = key.fingerprint();
let path = self.row_path(&fingerprint);
if !path.exists() {
return Ok(None);
}
match read_row(&path, &fingerprint) {
Ok(vector) => Ok(Some(vector)),
Err(e) => Err(e),
}
}
pub fn put_batch(&mut self, entries: &[(CacheKey, Vec<f32>)]) -> Result<usize, CacheError> {
if entries.is_empty() {
return Ok(0);
}
let (encoded, skipped_oversized) = self.encode_batch_entries(entries);
if skipped_oversized > 0 {
self.telemetry
.record_entry_rejection(skipped_oversized * 1024);
}
if encoded.is_empty() {
return Ok(0);
}
let batch_bytes: u64 = encoded.iter().map(|(_, bytes, _)| bytes.len() as u64).sum();
self.enforce_byte_budget(batch_bytes)?;
let written = self.write_batch_rows(&encoded)?;
for (fingerprint, _, model_digest) in &encoded {
let fingerprint_hex = hex_encode(fingerprint);
let model_digest_hex = hex_encode(model_digest);
self.model_index.insert(&fingerprint_hex, &model_digest_hex);
}
let _ = self.persist_model_index();
Ok(written)
}
fn write_batch_rows(
&mut self,
encoded: &[([u8; 32], Vec<u8>, [u8; 32])],
) -> Result<usize, CacheError> {
let mut created_dirs: HashSet<PathBuf> = HashSet::new();
let mut written = 0usize;
for (fingerprint, row_bytes, _) in encoded {
let final_path = self.row_path(fingerprint);
if let Some(parent) = final_path.parent() {
if created_dirs.insert(parent.to_path_buf()) {
fs::create_dir_all(parent)?;
}
}
let staging_path = staging_path_for(&final_path);
{
let file = fs::File::create(&staging_path)?;
let mut writer = BufWriter::new(file);
writer.write_all(row_bytes)?;
writer.flush()?;
}
match fs::rename(&staging_path, &final_path) {
Ok(()) => {
written += 1;
}
Err(_) if final_path.exists() => {
let _ = fs::remove_file(&staging_path);
}
Err(e) => {
let _ = fs::remove_file(&staging_path);
return Err(CacheError::Io(e));
}
}
}
Ok(written)
}
fn encode_batch_entries(
&self,
entries: &[(CacheKey, Vec<f32>)],
) -> (Vec<([u8; 32], Vec<u8>, [u8; 32])>, u64) {
let mut encoded: Vec<([u8; 32], Vec<u8>, [u8; 32])> = Vec::with_capacity(entries.len());
let mut seen: HashSet<[u8; 32]> = HashSet::new();
let mut skipped_oversized = 0u64;
for (key, vector) in entries {
let dim = key.output_dimensions as usize;
if vector.len() != dim || vector.is_empty() {
continue;
}
let fingerprint = key.fingerprint();
if !seen.insert(fingerprint) {
continue;
}
if self.row_path(&fingerprint).exists() {
continue;
}
let row_bytes = encode_row(&fingerprint, dim, vector, None);
if row_bytes.len() as u64 > self.config.max_entry_bytes {
skipped_oversized += 1;
continue;
}
encoded.push((fingerprint, row_bytes, key.model_digest));
}
(encoded, skipped_oversized)
}
pub fn add_reference(&mut self, fingerprint: &[u8; 32], project_id: &str, generation: u64) {
self.refs.add(fingerprint, project_id, generation);
}
pub fn remove_reference(&mut self, fingerprint: &[u8; 32], project_id: &str, generation: u64) {
self.refs.remove(fingerprint, project_id, generation);
}
pub fn is_referenced(&self, fingerprint: &[u8; 32]) -> bool {
self.refs.is_referenced(fingerprint)
}
pub fn row_count(&self) -> Result<usize, CacheError> {
let rows_dir = self.root.join("rows");
if !rows_dir.exists() {
return Ok(0);
}
let mut count = 0;
for entry in fs::read_dir(&rows_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
for sub_entry in fs::read_dir(entry.path())? {
let sub_entry = sub_entry?;
if is_row_file(&sub_entry.path()) {
count += 1;
}
}
}
}
Ok(count)
}
pub fn gc(&mut self) -> Result<CacheCompactionReport, CacheError> {
let mut report = CacheCompactionReport::default();
let rows_dir = self.root.join("rows");
if !rows_dir.exists() {
return Ok(report);
}
for entry in fs::read_dir(&rows_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
for sub_entry in fs::read_dir(entry.path())? {
let path = sub_entry?.path();
if !path.is_file() {
continue;
}
if hex_decode(&path.file_name().unwrap_or_default().to_string_lossy()).is_some() {
self.gc_row(&path, &mut report);
continue;
}
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
if fs::remove_file(&path).is_ok() {
report.staging_files_removed += 1;
report.reclaimed_bytes += size;
}
}
}
let still_existing = self.retain_existing_refs();
self.model_index.retain_existing(&still_existing);
self.persist_compaction_sidecars()?;
Ok(report)
}
fn persist_compaction_sidecars(&self) -> Result<(), CacheError> {
self.persist_refs()?;
self.persist_telemetry()?;
self.persist_model_index()?;
Ok(())
}
fn gc_row(&mut self, path: &Path, report: &mut CacheCompactionReport) {
let Some(filename) = path.file_name() else {
return;
};
let Some(fingerprint) = hex_decode(&filename.to_string_lossy()) else {
return;
};
if self.refs.is_referenced(&fingerprint) {
report.rows_retained += 1;
return;
}
let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
let _ = fs::remove_file(path);
report.reclaimed_bytes += size;
report.rows_removed += 1;
self.telemetry.record_eviction(size);
}
fn retain_existing_refs(&mut self) -> HashSet<String> {
let tracked: Vec<String> = self.refs.refs.keys().cloned().collect();
let mut still_existing = HashSet::new();
for hex in tracked {
if let Some(fp) = hex_decode(&hex) {
if self.row_path(&fp).exists() {
still_existing.insert(hex);
}
}
}
self.refs.refs.retain(|k, _| still_existing.contains(k));
still_existing
}
pub fn persist_refs(&self) -> Result<(), CacheError> {
let path = self.root.join(REFS_FILENAME);
let tmp = path.with_extension("tmp");
let data = serde_json::to_vec_pretty(&self.refs)?;
fs::write(&tmp, &data)?;
fs::rename(&tmp, &path)?;
Ok(())
}
pub fn persist_telemetry(&self) -> Result<(), CacheError> {
let path = self.root.join(TELEMETRY_FILENAME);
let tmp = path.with_extension("tmp");
let data = serde_json::to_vec_pretty(&self.telemetry)?;
fs::write(&tmp, &data)?;
fs::rename(&tmp, &path)?;
Ok(())
}
pub fn telemetry(&self) -> &CacheTelemetry {
&self.telemetry
}
pub fn config(&self) -> &CacheConfig {
&self.config
}
fn compute_model_identity(&self) -> Result<String, CacheError> {
let rows_dir = self.root.join("rows");
if !rows_dir.exists() {
return Ok("none".to_string());
}
let mut distinct_digests: HashSet<String> = HashSet::new();
for entry in fs::read_dir(&rows_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
for sub_entry in fs::read_dir(entry.path())? {
let sub_entry = sub_entry?;
let path = sub_entry.path();
if path.extension().is_some_and(|ext| ext == "partial") || !path.is_file() {
continue;
}
let filename = sub_entry.file_name().to_string_lossy().to_string();
if let Some(digest_hex) = self.model_index.get(&filename) {
distinct_digests.insert(digest_hex.to_string());
}
}
}
if distinct_digests.is_empty() {
Ok("none".to_string())
} else if distinct_digests.len() == 1 {
Ok(distinct_digests.into_iter().next().unwrap())
} else {
Ok("multiple".to_string())
}
}
fn persist_model_index(&self) -> Result<(), CacheError> {
let path = self.root.join(MODEL_INDEX_FILENAME);
let tmp = path.with_extension("tmp");
let data = serde_json::to_vec_pretty(&self.model_index)?;
fs::write(&tmp, &data)?;
fs::rename(&tmp, &path)?;
Ok(())
}
fn evict_unreferenced(&mut self, bytes_needed: u64) -> Result<(), CacheError> {
let rows_dir = self.root.join("rows");
if !rows_dir.exists() {
return Ok(());
}
let candidates = self.eviction_candidates(&rows_dir)?;
self.evict_rows_until(candidates, bytes_needed);
self.persist_telemetry()?;
Ok(())
}
fn eviction_candidates(&self, rows_dir: &Path) -> Result<Vec<EvictionCandidate>, CacheError> {
let mut candidates: Vec<EvictionCandidate> = Vec::new();
for entry in fs::read_dir(rows_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
for path in row_file_paths(fs::read_dir(entry.path())?)? {
self.push_eviction_candidate(&path, &mut candidates);
}
}
candidates.sort_by_key(|a| a.mtime);
Ok(candidates)
}
fn push_eviction_candidate(&self, path: &Path, candidates: &mut Vec<EvictionCandidate>) {
let Some(filename) = path.file_name() else {
return;
};
let Some(fingerprint) = hex_decode(&filename.to_string_lossy()) else {
return;
};
if self.refs.is_referenced(&fingerprint) {
return;
}
let Ok(meta) = fs::metadata(path) else {
return;
};
let mtime = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let size = meta.len();
candidates.push(EvictionCandidate {
path: path.to_path_buf(),
mtime,
size,
});
}
fn evict_rows_until(&mut self, candidates: Vec<EvictionCandidate>, bytes_needed: u64) {
let mut reclaimed = 0u64;
for row in candidates {
if reclaimed >= bytes_needed {
break;
}
match fs::remove_file(&row.path) {
Ok(()) => {
reclaimed += row.size;
self.telemetry.record_eviction(row.size);
}
Err(_) => continue,
}
}
}
pub fn cache_stats(&self) -> Result<CacheStatsReport, CacheError> {
let cache_bytes = self.total_bytes()?;
let row_count = self.row_count()?;
let tracked_references = self.refs.tracked_count();
let hit_ratio = self.telemetry.hit_ratio();
let model_identity = self.compute_model_identity()?;
Ok(CacheStatsReport {
cache_bytes,
row_count,
telemetry: self.telemetry.clone(),
hit_ratio,
max_bytes: self.config.max_bytes,
max_entry_bytes: self.config.max_entry_bytes,
tracked_references,
model_identity,
})
}
pub fn project_refs(&self) -> &ProjectRefs {
&self.refs
}
pub fn total_bytes(&self) -> Result<u64, CacheError> {
let rows_dir = self.root.join("rows");
if !rows_dir.exists() {
return Ok(0);
}
let mut total = 0u64;
for entry in fs::read_dir(&rows_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
for sub_entry in fs::read_dir(entry.path())? {
let sub_entry = sub_entry?;
let path = sub_entry.path();
if is_row_file(&path) {
total += fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
}
}
}
}
Ok(total)
}
}
fn staging_path_for(final_path: &Path) -> PathBuf {
static STAGING_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = STAGING_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut name = final_path
.file_name()
.map(std::ffi::OsString::from)
.unwrap_or_default();
name.push(format!(".{}.{}.partial", std::process::id(), seq));
final_path.with_file_name(name)
}
fn is_row_file(path: &Path) -> bool {
path.is_file()
&& path
.file_name()
.and_then(|name| name.to_str())
.and_then(hex_decode)
.is_some()
}
fn row_file_paths(shard: fs::ReadDir) -> Result<Vec<PathBuf>, CacheError> {
let mut paths = Vec::new();
for sub_entry in shard {
let sub_entry = sub_entry?;
let path = sub_entry.path();
if !is_row_file(&path) {
continue;
}
paths.push(path);
}
Ok(paths)
}
pub fn encode_row(
fingerprint: &[u8; 32],
dim: usize,
vector: &[f32],
debug_text: Option<&str>,
) -> Vec<u8> {
assert_eq!(vector.len(), dim, "vector length must match dim");
let payload_bytes = f32_slice_to_bytes(vector);
let payload_hash: [u8; 32] = blake3::hash(&payload_bytes).into();
let debug_bytes = debug_text.map(str::as_bytes).unwrap_or(&[]);
let mut buf = Vec::with_capacity(
ROW_HEADER_LEN
+ payload_bytes.len()
+ if debug_bytes.is_empty() {
0
} else {
4 + debug_bytes.len()
},
);
buf.extend_from_slice(ROW_MAGIC);
buf.push(ROW_VERSION);
buf.extend_from_slice(&[0u8; ROW_PAD_LEN]);
buf.extend_from_slice(fingerprint);
buf.extend_from_slice(&(dim as u32).to_le_bytes());
buf.extend_from_slice(&payload_hash);
buf.extend_from_slice(&payload_bytes);
if !debug_bytes.is_empty() {
buf.extend_from_slice(&(debug_bytes.len() as u32).to_le_bytes());
buf.extend_from_slice(debug_bytes);
}
buf
}
pub fn read_row(path: &Path, expected_fingerprint: &[u8; 32]) -> Result<Vec<f32>, CacheError> {
let bytes = fs::read(path)?;
if bytes.len() < ROW_HEADER_LEN {
return Err(CacheError::BadRow(format!(
"row truncated: {} bytes, need at least {} for header",
bytes.len(),
ROW_HEADER_LEN
)));
}
let mut magic = [0u8; 9];
magic.copy_from_slice(&bytes[0..9]);
if &magic != ROW_MAGIC {
return Err(CacheError::BadRow(format!("bad magic: got {magic:?}")));
}
let version = bytes[9];
if version != ROW_VERSION {
return Err(CacheError::BadRow(format!(
"unsupported version: got {version}, expected {ROW_VERSION}"
)));
}
let mut stored_fingerprint = [0u8; 32];
stored_fingerprint.copy_from_slice(&bytes[12..44]);
if &stored_fingerprint != expected_fingerprint {
return Err(CacheError::BadRow(
"fingerprint mismatch: stored fingerprint does not match expected".to_string(),
));
}
let mut dim_bytes = [0u8; 4];
dim_bytes.copy_from_slice(&bytes[44..48]);
let dim = u32::from_le_bytes(dim_bytes) as usize;
let mut stored_hash = [0u8; 32];
stored_hash.copy_from_slice(&bytes[48..80]);
let expected_payload_len = dim * std::mem::size_of::<f32>();
let trailing = &bytes[ROW_HEADER_LEN..];
if trailing.len() < expected_payload_len {
return Err(CacheError::BadRow(format!(
"payload length mismatch: got {}, need at least {}",
trailing.len(),
expected_payload_len
)));
}
let vector_bytes = &trailing[..expected_payload_len];
let computed_hash: [u8; 32] = blake3::hash(vector_bytes).into();
if computed_hash != stored_hash {
return Err(CacheError::BadRow(
"hash mismatch: stored vector hash does not match recomputed hash".to_string(),
));
}
Ok(bytes_to_f32_vec(vector_bytes))
}
fn f32_slice_to_bytes(slice: &[f32]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(std::mem::size_of_val(slice));
for &val in slice {
bytes.extend_from_slice(&val.to_le_bytes());
}
bytes
}
fn bytes_to_f32_vec(bytes: &[u8]) -> Vec<f32> {
let mut result = Vec::with_capacity(bytes.len() / std::mem::size_of::<f32>());
for chunk in bytes.chunks_exact(4) {
let mut buf = [0u8; 4];
buf.copy_from_slice(chunk);
result.push(f32::from_le_bytes(buf));
}
result
}
pub fn hex_encode(hash: &[u8; 32]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(64);
for byte in hash {
write!(&mut s, "{byte:02x}").expect("formatting into String never fails");
}
s
}
pub fn hex_decode(hex: &str) -> Option<[u8; 32]> {
if hex.len() != 64 {
return None;
}
let mut hash = [0u8; 32];
let bytes = hex.as_bytes();
for (i, byte) in hash.iter_mut().enumerate() {
let hi = hex_nibble(bytes[i * 2])?;
let lo = hex_nibble(bytes[i * 2 + 1])?;
*byte = (hi << 4) | lo;
}
Some(hash)
}
fn hex_nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
pub fn is_debug_escape_hatch_active(config_debug_mode: bool) -> bool {
if config_debug_mode {
return true;
}
if std::env::var(DEBUG_ENV_VAR)
.map(|v| {
matches!(
v.trim().to_lowercase().as_str(),
"1" | "true" | "yes" | "on" | "enable" | "enabled"
)
})
.unwrap_or(false)
{
return true;
}
crate::feature_flags::FeatureFlag::DebugEscapeHatch.is_enabled()
}
fn load_refs(root: &Path) -> Result<ProjectRefs, CacheError> {
let path = root.join(REFS_FILENAME);
if !path.exists() {
return Ok(ProjectRefs::default());
}
let data = fs::read(&path)?;
Ok(serde_json::from_slice(&data)?)
}
fn load_telemetry(root: &Path) -> Result<CacheTelemetry, CacheError> {
let path = root.join(TELEMETRY_FILENAME);
if !path.exists() {
return Ok(CacheTelemetry::default());
}
let data = fs::read(&path)?;
Ok(serde_json::from_slice(&data)?)
}
fn load_model_index(root: &Path) -> Result<ModelIndex, CacheError> {
let path = root.join(MODEL_INDEX_FILENAME);
if !path.exists() {
return Ok(ModelIndex::default());
}
let data = fs::read(&path)?;
Ok(serde_json::from_slice(&data)?)
}
#[cfg(test)]
#[path = "store_test.rs"]
mod tests;