use std::io::{self, Read, Write};
use thiserror::Error;
const MAGIC: &[u8; 8] = b"OXICACHE";
const FORMAT_VERSION: u16 = 1;
#[derive(Debug, Error)]
pub enum SerializeError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("invalid magic header: expected 'OXICACHE', got {0:?}")]
InvalidMagic([u8; 8]),
#[error("unsupported format version {0}; expected {FORMAT_VERSION}")]
UnsupportedVersion(u16),
#[error("key at entry {0} is not valid UTF-8: {1}")]
InvalidKeyUtf8(usize, std::string::FromUtf8Error),
#[error("entry {index} value length {actual} exceeds safety limit {limit}")]
ValueTooLarge {
index: usize,
actual: u32,
limit: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheRecord {
pub key: String,
pub value: Vec<u8>,
pub ttl_secs: u64,
pub priority: u32,
}
impl CacheRecord {
pub fn new(key: impl Into<String>, value: Vec<u8>) -> Self {
Self {
key: key.into(),
value,
ttl_secs: 0,
priority: 0,
}
}
pub fn with_ttl(mut self, ttl_secs: u64) -> Self {
self.ttl_secs = ttl_secs;
self
}
pub fn with_priority(mut self, priority: u32) -> Self {
self.priority = priority;
self
}
}
pub fn serialize<W: Write>(writer: &mut W, records: &[CacheRecord]) -> Result<(), SerializeError> {
writer.write_all(MAGIC)?;
writer.write_all(&FORMAT_VERSION.to_le_bytes())?;
let flags: u16 = 0;
writer.write_all(&flags.to_le_bytes())?;
let n = records.len() as u32;
writer.write_all(&n.to_le_bytes())?;
for rec in records {
let key_bytes = rec.key.as_bytes();
writer.write_all(&(key_bytes.len() as u32).to_le_bytes())?;
writer.write_all(key_bytes)?;
writer.write_all(&(rec.value.len() as u32).to_le_bytes())?;
writer.write_all(&rec.value)?;
writer.write_all(&rec.priority.to_le_bytes())?;
writer.write_all(&rec.ttl_secs.to_le_bytes())?;
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct DeserializeConfig {
pub max_value_bytes: u32,
pub max_records: u32,
}
impl Default for DeserializeConfig {
fn default() -> Self {
Self {
max_value_bytes: 512 * 1024 * 1024, max_records: u32::MAX,
}
}
}
pub fn deserialize<R: Read>(reader: &mut R) -> Result<Vec<CacheRecord>, SerializeError> {
deserialize_with_config(reader, &DeserializeConfig::default())
}
pub fn deserialize_with_config<R: Read>(
reader: &mut R,
config: &DeserializeConfig,
) -> Result<Vec<CacheRecord>, SerializeError> {
let mut magic = [0u8; 8];
reader.read_exact(&mut magic)?;
if &magic != MAGIC {
return Err(SerializeError::InvalidMagic(magic));
}
let mut ver_buf = [0u8; 2];
reader.read_exact(&mut ver_buf)?;
let version = u16::from_le_bytes(ver_buf);
if version != FORMAT_VERSION {
return Err(SerializeError::UnsupportedVersion(version));
}
let mut flags_buf = [0u8; 2];
reader.read_exact(&mut flags_buf)?;
let mut n_buf = [0u8; 4];
reader.read_exact(&mut n_buf)?;
let n = u32::from_le_bytes(n_buf);
let to_read = n.min(config.max_records);
let mut records = Vec::with_capacity(to_read as usize);
for idx in 0..n as usize {
let mut klen_buf = [0u8; 4];
reader.read_exact(&mut klen_buf)?;
let key_len = u32::from_le_bytes(klen_buf) as usize;
let mut key_bytes = vec![0u8; key_len];
reader.read_exact(&mut key_bytes)?;
let key =
String::from_utf8(key_bytes).map_err(|e| SerializeError::InvalidKeyUtf8(idx, e))?;
let mut vlen_buf = [0u8; 4];
reader.read_exact(&mut vlen_buf)?;
let val_len = u32::from_le_bytes(vlen_buf);
if val_len > config.max_value_bytes {
return Err(SerializeError::ValueTooLarge {
index: idx,
actual: val_len,
limit: config.max_value_bytes,
});
}
let mut value = vec![0u8; val_len as usize];
reader.read_exact(&mut value)?;
let mut prio_buf = [0u8; 4];
reader.read_exact(&mut prio_buf)?;
let priority = u32::from_le_bytes(prio_buf);
let mut ttl_buf = [0u8; 8];
reader.read_exact(&mut ttl_buf)?;
let ttl_secs = u64::from_le_bytes(ttl_buf);
if (idx as u32) < config.max_records {
records.push(CacheRecord {
key,
value,
ttl_secs,
priority,
});
}
}
Ok(records)
}
pub fn save_to_file(path: &std::path::Path, records: &[CacheRecord]) -> Result<(), SerializeError> {
let mut file = std::fs::File::create(path)?;
serialize(&mut file, records)
}
pub fn load_from_file(path: &std::path::Path) -> Result<Vec<CacheRecord>, SerializeError> {
let mut file = std::fs::File::open(path)?;
deserialize(&mut file)
}
pub fn load_from_file_with_config(
path: &std::path::Path,
config: &DeserializeConfig,
) -> Result<Vec<CacheRecord>, SerializeError> {
let mut file = std::fs::File::open(path)?;
deserialize_with_config(&mut file, config)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn roundtrip(records: &[CacheRecord]) -> Vec<CacheRecord> {
let mut buf = Vec::new();
serialize(&mut buf, records).expect("serialize should succeed");
let mut cursor = Cursor::new(&buf);
deserialize(&mut cursor).expect("deserialize should succeed")
}
#[test]
fn test_empty_roundtrip() {
let records: Vec<CacheRecord> = Vec::new();
let restored = roundtrip(&records);
assert!(restored.is_empty());
}
#[test]
fn test_single_record_roundtrip() {
let records = vec![CacheRecord::new("key-001", b"hello world".to_vec())];
let restored = roundtrip(&records);
assert_eq!(restored.len(), 1);
assert_eq!(restored[0].key, "key-001");
assert_eq!(restored[0].value, b"hello world");
assert_eq!(restored[0].ttl_secs, 0);
assert_eq!(restored[0].priority, 0);
}
#[test]
fn test_multiple_records_roundtrip() {
let records: Vec<CacheRecord> = (0..20u32)
.map(|i| {
CacheRecord::new(format!("seg-{i:04}"), vec![i as u8; 128])
.with_ttl(300)
.with_priority(i % 5)
})
.collect();
let restored = roundtrip(&records);
assert_eq!(restored.len(), records.len());
for (orig, rest) in records.iter().zip(restored.iter()) {
assert_eq!(orig, rest);
}
}
#[test]
fn test_ttl_and_priority_roundtrip() {
let rec = CacheRecord::new("manifest.m3u8", b"#EXTM3U".to_vec())
.with_ttl(30)
.with_priority(10);
let restored = roundtrip(std::slice::from_ref(&rec));
assert_eq!(restored[0].ttl_secs, 30);
assert_eq!(restored[0].priority, 10);
}
#[test]
fn test_binary_value_roundtrip() {
let value: Vec<u8> = (0u8..=255).collect();
let records = vec![CacheRecord::new("binary", value.clone())];
let restored = roundtrip(&records);
assert_eq!(restored[0].value, value);
}
#[test]
fn test_unicode_key_roundtrip() {
let records = vec![CacheRecord::new("媒体-segment-001", vec![1, 2, 3])];
let restored = roundtrip(&records);
assert_eq!(restored[0].key, "媒体-segment-001");
}
#[test]
fn test_invalid_magic() {
let garbage = b"GARBAGE_HEADER_DATA";
let mut cursor = Cursor::new(garbage);
let result = deserialize(&mut cursor);
assert!(
matches!(result, Err(SerializeError::InvalidMagic(_))),
"expected InvalidMagic, got {result:?}"
);
}
#[test]
fn test_wrong_version() {
let mut buf = Vec::new();
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&9999u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); let mut cursor = Cursor::new(&buf);
let result = deserialize(&mut cursor);
assert!(
matches!(result, Err(SerializeError::UnsupportedVersion(9999))),
"expected UnsupportedVersion"
);
}
#[test]
fn test_max_records_limit() {
let records: Vec<CacheRecord> = (0..10u32)
.map(|i| CacheRecord::new(format!("k{i}"), vec![i as u8]))
.collect();
let mut buf = Vec::new();
serialize(&mut buf, &records).expect("ok");
let config = DeserializeConfig {
max_records: 3,
..Default::default()
};
let mut cursor = Cursor::new(&buf);
let restored = deserialize_with_config(&mut cursor, &config).expect("ok");
assert_eq!(restored.len(), 3, "only 3 records should be restored");
}
#[test]
fn test_max_value_bytes_rejected() {
let records = vec![CacheRecord::new("big", vec![0u8; 1024])];
let mut buf = Vec::new();
serialize(&mut buf, &records).expect("ok");
let config = DeserializeConfig {
max_value_bytes: 128, ..Default::default()
};
let mut cursor = Cursor::new(&buf);
let result = deserialize_with_config(&mut cursor, &config);
assert!(
matches!(result, Err(SerializeError::ValueTooLarge { .. })),
"expected ValueTooLarge"
);
}
#[test]
fn test_file_save_load_roundtrip() {
let dir = std::env::temp_dir();
let path = dir.join("oximedia_cache_test_serialization.bin");
let records = vec![
CacheRecord::new("segment-1", b"data1".to_vec()).with_ttl(60),
CacheRecord::new("segment-2", b"data2".to_vec()).with_priority(5),
];
save_to_file(&path, &records).expect("save should succeed");
let restored = load_from_file(&path).expect("load should succeed");
assert_eq!(restored.len(), 2);
assert_eq!(restored[0].key, "segment-1");
assert_eq!(restored[1].priority, 5);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_empty_key_roundtrip() {
let records = vec![CacheRecord::new("", b"value".to_vec())];
let restored = roundtrip(&records);
assert_eq!(restored[0].key, "");
}
#[test]
fn test_empty_value_roundtrip() {
let records = vec![CacheRecord::new("empty-val", Vec::new())];
let restored = roundtrip(&records);
assert!(restored[0].value.is_empty());
}
#[test]
fn test_serialized_magic_prefix() {
let mut buf = Vec::new();
serialize(&mut buf, &[]).expect("ok");
assert_eq!(&buf[..8], MAGIC);
}
#[test]
fn test_cache_record_builder() {
let rec = CacheRecord::new("k", vec![1, 2])
.with_ttl(120)
.with_priority(7);
assert_eq!(rec.key, "k");
assert_eq!(rec.ttl_secs, 120);
assert_eq!(rec.priority, 7);
}
}