use std::time::{Duration, SystemTime, UNIX_EPOCH};
use super::CacheCodec;
use crate::backend::CacheRead;
use crate::error::CacheError;
pub const MAGIC: [u8; 3] = *b"THC";
pub const FORMAT_V1: u8 = 0x01;
pub const CODEC_POSTCARD: u8 = 0x01;
pub const CODEC_USER: u8 = 0x80;
pub const ENVELOPE_HEADER_LEN: usize = 21;
pub const LEGACY_REDIS_OVERHEAD: usize = 24;
pub fn wrap(codec_id: u8, expires_at_ms: u64, stale_until_ms: u64, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(ENVELOPE_HEADER_LEN + payload.len());
out.extend_from_slice(&MAGIC);
out.push(FORMAT_V1);
out.push(codec_id);
out.extend_from_slice(&expires_at_ms.to_le_bytes());
out.extend_from_slice(&stale_until_ms.to_le_bytes());
out.extend_from_slice(payload);
out
}
pub fn looks_like_v2(bytes: &[u8]) -> bool {
bytes.len() >= ENVELOPE_HEADER_LEN && bytes[0..3] == MAGIC && bytes[3] == FORMAT_V1
}
pub fn is_legacy_redis(bytes: &[u8]) -> bool {
bytes.len() >= LEGACY_REDIS_OVERHEAD
&& u64::from_le_bytes(bytes[0..8].try_into().unwrap())
== (bytes.len() - LEGACY_REDIS_OVERHEAD) as u64
}
pub fn decode_v2<C: CacheCodec>(bytes: &[u8], codec: &C) -> Result<CacheRead, CacheError> {
if bytes.len() < ENVELOPE_HEADER_LEN {
return Err(CacheError::Backend(format!(
"envelope too short: {} bytes",
bytes.len()
)));
}
if bytes[0..3] != MAGIC {
return Err(CacheError::Backend("missing envelope magic".to_string()));
}
if bytes[3] != FORMAT_V1 {
return Err(CacheError::Backend(format!(
"unsupported envelope format version {:#04x}",
bytes[3]
)));
}
if bytes[4] != C::CODEC_ID {
return Err(CacheError::Backend(format!(
"entry was written by codec id {:#04x}, this backend uses {:#04x}",
bytes[4],
C::CODEC_ID
)));
}
let expires_at_ms = u64::from_le_bytes(bytes[5..13].try_into().unwrap());
let stale_until_ms = u64::from_le_bytes(bytes[13..21].try_into().unwrap());
let entry = codec.decode(&bytes[ENVELOPE_HEADER_LEN..])?;
Ok(CacheRead {
entry,
expires_at: Some(unix_ms_to_system_time(expires_at_ms)),
stale_until: Some(unix_ms_to_system_time(stale_until_ms)),
})
}
pub fn read_stored<C: CacheCodec>(
bytes: &[u8],
codec: &C,
) -> Result<Option<CacheRead>, CacheError> {
let legacy_first = is_legacy_redis(bytes);
if legacy_first {
if let Some(read) = try_legacy(bytes) {
return Ok(Some(read));
}
}
if looks_like_v2(bytes) {
match decode_v2(bytes, codec) {
Ok(read) => return Ok(Some(read)),
Err(err) => observe_decode_error("envelope", &err),
}
}
if !legacy_first {
if let Some(read) = try_legacy(bytes) {
return Ok(Some(read));
}
}
observe_decode_error(
"unrecognised",
&CacheError::Backend(format!(
"no decoder recognised the stored value ({} bytes)",
bytes.len()
)),
);
Ok(None)
}
#[cfg(feature = "legacy-bincode1-read")]
fn try_legacy(bytes: &[u8]) -> Option<CacheRead> {
match super::legacy::decode_legacy_redis(bytes) {
Ok(read) => Some(read),
Err(err) => {
observe_decode_error("legacy-bincode1", &err);
None
}
}
}
#[cfg(not(feature = "legacy-bincode1-read"))]
fn try_legacy(_bytes: &[u8]) -> Option<CacheRead> {
None
}
fn observe_decode_error(kind: &str, err: &CacheError) {
#[cfg(feature = "metrics")]
metrics::counter!("tower_http_cache.decode_error", "kind" => kind.to_string()).increment(1);
#[cfg(feature = "tracing")]
tracing::warn!(kind = %kind, error = %err, "cache_entry_decode_failed");
let _ = (kind, err);
}
pub fn unix_ms_to_system_time(ms: u64) -> SystemTime {
UNIX_EPOCH + Duration::from_millis(ms)
}
#[cfg(feature = "redis-backend")]
pub(crate) fn current_millis() -> Result<u64, CacheError> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|err| CacheError::Backend(err.to_string()))?
.as_millis() as u64)
}
#[cfg(feature = "redis-backend")]
pub(crate) fn duration_millis(duration: Duration) -> u64 {
duration.as_millis().min(u64::MAX as u128) as u64
}