use bytemuck::{Pod, Zeroable};
use bytes::Bytes;
use chrono::{DateTime, Utc};
use hitbox_core::{CacheValue, Raw};
use std::io::{self, Write};
use crate::{BackendError, BackendResult};
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
struct EnvelopeHeader {
discriminant: u8,
_pad1: [u8; 3],
l1_len: u32,
l2_len: u32,
_pad2: [u8; 4],
expire_secs: i64,
expire_nanos: u32,
_pad3: [u8; 4],
stale_secs: i64,
stale_nanos: u32,
_pad4: [u8; 4],
}
impl EnvelopeHeader {
const SIZE: usize = std::mem::size_of::<Self>();
fn new_l1(l1_len: usize, expire: Option<DateTime<Utc>>, stale: Option<DateTime<Utc>>) -> Self {
let (expire_secs, expire_nanos) = Self::encode_timestamp(expire);
let (stale_secs, stale_nanos) = Self::encode_timestamp(stale);
Self {
discriminant: 0,
_pad1: [0; 3],
l1_len: l1_len as u32,
l2_len: 0,
_pad2: [0; 4],
expire_secs,
expire_nanos,
_pad3: [0; 4],
stale_secs,
stale_nanos,
_pad4: [0; 4],
}
}
fn new_l2(l2_len: usize, expire: Option<DateTime<Utc>>, stale: Option<DateTime<Utc>>) -> Self {
let (expire_secs, expire_nanos) = Self::encode_timestamp(expire);
let (stale_secs, stale_nanos) = Self::encode_timestamp(stale);
Self {
discriminant: 1,
_pad1: [0; 3],
l1_len: 0,
l2_len: l2_len as u32,
_pad2: [0; 4],
expire_secs,
expire_nanos,
_pad3: [0; 4],
stale_secs,
stale_nanos,
_pad4: [0; 4],
}
}
fn new_both(
l1_len: usize,
l2_len: usize,
expire: Option<DateTime<Utc>>,
stale: Option<DateTime<Utc>>,
) -> Self {
let (expire_secs, expire_nanos) = Self::encode_timestamp(expire);
let (stale_secs, stale_nanos) = Self::encode_timestamp(stale);
Self {
discriminant: 2,
_pad1: [0; 3],
l1_len: l1_len as u32,
l2_len: l2_len as u32,
_pad2: [0; 4],
expire_secs,
expire_nanos,
_pad3: [0; 4],
stale_secs,
stale_nanos,
_pad4: [0; 4],
}
}
fn encode_timestamp(timestamp: Option<DateTime<Utc>>) -> (i64, u32) {
match timestamp {
Some(dt) => (dt.timestamp(), dt.timestamp_subsec_nanos()),
None => (0, 0),
}
}
fn decode_timestamp(secs: i64, nanos: u32) -> Option<DateTime<Utc>> {
if secs == 0 && nanos == 0 {
None
} else {
DateTime::from_timestamp(secs, nanos)
}
}
fn as_bytes(&self) -> &[u8] {
bytemuck::bytes_of(self)
}
fn from_bytes(data: &[u8]) -> Result<Self, io::Error> {
if data.len() < Self::SIZE {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"Envelope header too short: expected {} bytes, got {}",
Self::SIZE,
data.len()
),
));
}
Ok(*bytemuck::from_bytes::<Self>(&data[..Self::SIZE]))
}
fn decode_expire(&self) -> Option<DateTime<Utc>> {
Self::decode_timestamp(self.expire_secs, self.expire_nanos)
}
fn decode_stale(&self) -> Option<DateTime<Utc>> {
Self::decode_timestamp(self.stale_secs, self.stale_nanos)
}
}
#[derive(Debug, Clone)]
pub(crate) enum CompositionEnvelope {
L1(CacheValue<Raw>),
L2(CacheValue<Raw>),
Both {
l1: CacheValue<Raw>,
l2: CacheValue<Raw>,
},
}
impl CompositionEnvelope {
pub(crate) fn serialize(&self) -> BackendResult<Bytes> {
match self {
CompositionEnvelope::L1(value) => {
let header =
EnvelopeHeader::new_l1(value.data().len(), value.expire(), value.stale());
let total_size = EnvelopeHeader::SIZE + value.data().len();
let mut buf = Vec::with_capacity(total_size);
buf.write_all(header.as_bytes())
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
buf.write_all(value.data())
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
Ok(Bytes::from(buf))
}
CompositionEnvelope::L2(value) => {
let header =
EnvelopeHeader::new_l2(value.data().len(), value.expire(), value.stale());
let total_size = EnvelopeHeader::SIZE + value.data().len();
let mut buf = Vec::with_capacity(total_size);
buf.write_all(header.as_bytes())
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
buf.write_all(value.data())
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
Ok(Bytes::from(buf))
}
CompositionEnvelope::Both { l1, l2 } => {
let header = EnvelopeHeader::new_both(
l1.data().len(),
l2.data().len(),
l1.expire(),
l1.stale(),
);
let total_size = EnvelopeHeader::SIZE + l1.data().len() + l2.data().len();
let mut buf = Vec::with_capacity(total_size);
buf.write_all(header.as_bytes())
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
buf.write_all(l1.data())
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
buf.write_all(l2.data())
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
Ok(Bytes::from(buf))
}
}
}
pub(crate) fn deserialize(data: &[u8]) -> BackendResult<Self> {
let header = EnvelopeHeader::from_bytes(data)
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
let payload_start = EnvelopeHeader::SIZE;
match header.discriminant {
0 => {
let l1_len = header.l1_len as usize;
if data.len() < payload_start + l1_len {
return Err(BackendError::InternalError(Box::new(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"L1 envelope data too short: expected {} bytes, got {}",
payload_start + l1_len,
data.len()
),
))));
}
let l1_data = Bytes::copy_from_slice(&data[payload_start..payload_start + l1_len]);
Ok(CompositionEnvelope::L1(CacheValue::new(
l1_data,
header.decode_expire(),
header.decode_stale(),
)))
}
1 => {
let l2_len = header.l2_len as usize;
if data.len() < payload_start + l2_len {
return Err(BackendError::InternalError(Box::new(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"L2 envelope data too short: expected {} bytes, got {}",
payload_start + l2_len,
data.len()
),
))));
}
let l2_data = Bytes::copy_from_slice(&data[payload_start..payload_start + l2_len]);
Ok(CompositionEnvelope::L2(CacheValue::new(
l2_data,
header.decode_expire(),
header.decode_stale(),
)))
}
2 => {
let l1_len = header.l1_len as usize;
let l2_len = header.l2_len as usize;
let l1_end = payload_start + l1_len;
let l2_end = l1_end + l2_len;
if data.len() < l2_end {
return Err(BackendError::InternalError(Box::new(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"Both envelope data too short: expected {} bytes, got {}",
l2_end,
data.len()
),
))));
}
let l1_data = Bytes::copy_from_slice(&data[payload_start..l1_end]);
let l2_data = Bytes::copy_from_slice(&data[l1_end..l2_end]);
Ok(CompositionEnvelope::Both {
l1: CacheValue::new(l1_data, header.decode_expire(), header.decode_stale()),
l2: CacheValue::new(l2_data, header.decode_expire(), header.decode_stale()),
})
}
_ => Err(BackendError::InternalError(Box::new(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid envelope discriminant: {}", header.discriminant),
)))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_envelope_header_size() {
assert_eq!(EnvelopeHeader::SIZE, 48);
}
#[test]
fn test_roundtrip_l1() {
let data = Bytes::from(vec![1, 2, 3, 4, 5]);
let expire = Some(Utc::now() + Duration::hours(1));
let stale = None;
let envelope = CompositionEnvelope::L1(CacheValue::new(data.clone(), expire, stale));
let serialized = envelope.serialize().unwrap();
let deserialized = CompositionEnvelope::deserialize(&serialized).unwrap();
match deserialized {
CompositionEnvelope::L1(value) => {
assert_eq!(*value.data(), data);
assert!(value.expire().is_some());
assert!(value.stale().is_none());
}
_ => panic!("Expected L1 variant"),
}
}
#[test]
fn test_roundtrip_both() {
let l1_data = Bytes::from(vec![1, 2, 3]);
let l2_data = Bytes::from(vec![4, 5, 6, 7, 8]);
let expire = Some(Utc::now() + Duration::hours(1));
let stale = Some(Utc::now() + Duration::minutes(30));
let envelope = CompositionEnvelope::Both {
l1: CacheValue::new(l1_data.clone(), expire, stale),
l2: CacheValue::new(l2_data.clone(), expire, stale),
};
let serialized = envelope.serialize().unwrap();
let deserialized = CompositionEnvelope::deserialize(&serialized).unwrap();
match deserialized {
CompositionEnvelope::Both { l1, l2 } => {
assert_eq!(*l1.data(), l1_data);
assert_eq!(*l2.data(), l2_data);
assert!(l1.expire().is_some());
assert!(l1.stale().is_some());
}
_ => panic!("Expected Both variant"),
}
}
#[test]
fn test_large_payload() {
let l1_data = Bytes::from(vec![0u8; 100_000]);
let l2_data = Bytes::from(vec![1u8; 100_000]);
let envelope = CompositionEnvelope::Both {
l1: CacheValue::new(l1_data.clone(), None, None),
l2: CacheValue::new(l2_data.clone(), None, None),
};
let serialized = envelope.serialize().unwrap();
let deserialized = CompositionEnvelope::deserialize(&serialized).unwrap();
match deserialized {
CompositionEnvelope::Both { l1, l2 } => {
assert_eq!(l1.data().len(), 100_000);
assert_eq!(l2.data().len(), 100_000);
}
_ => panic!("Expected Both variant"),
}
}
}