use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::blob::kind::BlobKind;
pub const CACHE_MAGIC: [u8; 4] = *b"CNC\0";
pub const CACHE_SEGMENT_VERSION: u32 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CacheEntryKind {
Pipeline,
Shader,
Payload,
Expansion,
Thumbnail,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheEntry {
pub kind: CacheEntryKind,
pub key: String,
pub offset: u64,
pub len: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheMeta {
pub toolchain: String,
pub entries: Vec<CacheEntry>,
}
impl CacheMeta {
pub fn find(&self, kind: CacheEntryKind, key: &str) -> Option<&CacheEntry> {
self.entries.iter().find(|e| e.kind == kind && e.key == key)
}
}
impl BlobKind for CacheMeta {
const MAGIC: [u8; 4] = CACHE_MAGIC;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::blob::schema::BlobMeta;
use crate::blob::{BlobError, encode_cnb, parse_cnb};
use alloc::vec;
fn entry(kind: CacheEntryKind, key: &str, offset: u64, len: u64) -> CacheEntry {
CacheEntry {
kind,
key: String::from(key),
offset,
len,
}
}
#[test]
fn a_segment_round_trips_its_index_and_payload() {
let meta = CacheMeta {
toolchain: String::from("slang 2026.1"),
entries: vec![
entry(CacheEntryKind::Pipeline, "vk-aa", 0, 3),
entry(CacheEntryKind::Shader, "deadbeef", 3, 2),
],
};
let image = encode_cnb(CACHE_SEGMENT_VERSION, &meta, &[1, 2, 3, 4, 5]).unwrap();
let (got, payload_start) =
parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &image).expect("parse");
assert_eq!(got, meta);
assert_eq!(&image[payload_start..], &[1, 2, 3, 4, 5]);
}
#[test]
fn a_world_blob_is_not_a_cache_segment() {
assert_ne!(CacheMeta::MAGIC, BlobMeta::MAGIC);
let world = encode_cnb(CACHE_SEGMENT_VERSION, &BlobMeta::default(), &[]).unwrap();
assert_eq!(
parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &world),
Err(BlobError::BadMagic)
);
}
#[test]
fn a_segment_of_another_version_is_rejected() {
let image = encode_cnb(CACHE_SEGMENT_VERSION - 1, &CacheMeta::default(), &[]).unwrap();
assert_eq!(
parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &image),
Err(BlobError::ValidityMismatch(CACHE_SEGMENT_VERSION - 1))
);
}
#[test]
fn a_kind_separates_two_entries_sharing_one_key() {
let meta = CacheMeta {
toolchain: String::new(),
entries: vec![
entry(CacheEntryKind::Payload, "cafe", 0, 1),
entry(CacheEntryKind::Expansion, "cafe", 1, 1),
],
};
assert_eq!(
meta.find(CacheEntryKind::Payload, "cafe"),
Some(&meta.entries[0])
);
assert_eq!(
meta.find(CacheEntryKind::Expansion, "cafe"),
Some(&meta.entries[1])
);
}
#[test]
fn lookup_matches_on_both_kind_and_key() {
let meta = CacheMeta {
toolchain: String::new(),
entries: vec![entry(CacheEntryKind::Pipeline, "vk-aa", 0, 3)],
};
assert_eq!(
meta.find(CacheEntryKind::Pipeline, "vk-aa"),
Some(&meta.entries[0])
);
assert_eq!(meta.find(CacheEntryKind::Pipeline, "vk-bb"), None);
assert_eq!(meta.find(CacheEntryKind::Shader, "vk-aa"), None);
}
}