use crate::compression::Compression;
use crate::error::{KafkoError, Result};
use bytes::{Buf, BufMut, Bytes, BytesMut};
use std::cell::RefCell;
const KEY_NULL_SENTINEL: u32 = u32::MAX;
const MIN_TOTAL_LEN: usize = 4 + 1 + 8 + 4 + 4;
thread_local! {
static ENCODE_COMPRESS_SCRATCH: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
timestamp_ms: i64,
key: Option<Bytes>,
value: Bytes,
}
impl Record {
pub fn new(timestamp_ms: i64, key: Option<Bytes>, value: Bytes) -> Self {
Self {
timestamp_ms,
key,
value,
}
}
pub fn timestamp_ms(&self) -> i64 {
self.timestamp_ms
}
pub fn key(&self) -> Option<&Bytes> {
self.key.as_ref()
}
pub fn value(&self) -> &Bytes {
&self.value
}
pub fn wire_size(&self) -> usize {
let key_part = match &self.key {
None => 4,
Some(k) => 4 + k.len(),
};
4 + 4 + 1 + 8 + key_part + 4 + self.value.len()
}
pub fn encode(self, out: &mut BytesMut) -> usize {
self.encode_with(out, Compression::None)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn encode_with(self, out: &mut BytesMut, compression: Compression) -> usize {
match compression {
Compression::None => self.encode_inner(out, compression, None),
Compression::Lz4 | Compression::Zstd => ENCODE_COMPRESS_SCRATCH.with(|s| {
let mut scratch = s.borrow_mut();
compression.compress(&self.value, &mut scratch);
self.encode_inner(out, compression, Some(&scratch[..]))
}),
}
}
fn encode_inner(
self,
out: &mut BytesMut,
compression: Compression,
val_bytes_override: Option<&[u8]>,
) -> usize {
let start_len = out.len();
let flag = compression.flag();
let val_bytes: &[u8] = val_bytes_override.unwrap_or(&self.value);
let key_part = match &self.key {
None => 4,
Some(k) => 4 + k.len(),
};
let val_part = 4 + val_bytes.len();
let payload_len = 1 + 8 + key_part + val_part;
let total_len = 4 + payload_len;
out.reserve(4 + total_len);
out.put_u32(total_len as u32);
let crc_pos = out.len();
out.put_u32(0);
let payload_start = out.len();
out.put_u8(flag);
out.put_i64(self.timestamp_ms);
match &self.key {
None => out.put_u32(KEY_NULL_SENTINEL),
Some(k) => {
out.put_u32(k.len() as u32);
out.put_slice(k);
}
}
out.put_u32(val_bytes.len() as u32);
out.put_slice(val_bytes);
let crc = crc32fast::hash(&out[payload_start..]);
out[crc_pos..crc_pos + 4].copy_from_slice(&crc.to_be_bytes());
out.len() - start_len
}
pub fn decode(buf: &mut &[u8]) -> Result<Self> {
if buf.remaining() < 4 {
return Err(KafkoError::Truncated {
needed: 4 - buf.remaining(),
});
}
let total_len = buf.get_u32() as usize;
if total_len < MIN_TOTAL_LEN {
return Err(KafkoError::InvalidLength(total_len as u32));
}
if buf.remaining() < total_len {
return Err(KafkoError::Truncated {
needed: total_len - buf.remaining(),
});
}
let expected_crc = buf.get_u32();
let payload_len = total_len - 4;
let actual_crc = crc32fast::hash(&buf[..payload_len]);
if actual_crc != expected_crc {
return Err(KafkoError::CrcMismatch {
expected: expected_crc,
actual: actual_crc,
});
}
let flag = buf.get_u8();
let compression = Compression::from_flag(flag)?;
let timestamp_ms = buf.get_i64();
let key_len_raw = buf.get_u32();
let key = if key_len_raw == KEY_NULL_SENTINEL {
None
} else {
let key_len = key_len_raw as usize;
if buf.remaining() < key_len {
return Err(KafkoError::InvalidLength(total_len as u32));
}
let bytes = Bytes::copy_from_slice(&buf[..key_len]);
buf.advance(key_len);
Some(bytes)
};
if buf.remaining() < 4 {
return Err(KafkoError::InvalidLength(total_len as u32));
}
let val_len = buf.get_u32() as usize;
if buf.remaining() < val_len {
return Err(KafkoError::InvalidLength(total_len as u32));
}
let raw_val: &[u8] = &buf[..val_len];
let value = match compression {
Compression::None => Bytes::copy_from_slice(raw_val),
Compression::Lz4 | Compression::Zstd => Bytes::from(compression.decompress(raw_val)?),
};
buf.advance(val_len);
Ok(Self {
timestamp_ms,
key,
value,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Record {
Record::new(
1_700_000_000_000,
Some(Bytes::from_static(b"key")),
Bytes::from_static(b"value"),
)
}
#[test]
fn getters_return_constructor_inputs() {
let r = sample();
assert_eq!(r.timestamp_ms(), 1_700_000_000_000);
assert_eq!(r.key(), Some(&Bytes::from_static(b"key")));
assert_eq!(r.value(), &Bytes::from_static(b"value"));
}
#[test]
fn roundtrip_with_key() {
let r = sample();
let expected = r.clone();
let expected_size = r.wire_size();
let mut buf = BytesMut::new();
let written = r.encode(&mut buf);
assert_eq!(buf.len(), expected_size);
assert_eq!(written, expected_size);
let mut slice: &[u8] = &buf;
let decoded = Record::decode(&mut slice).unwrap();
assert_eq!(decoded, expected);
assert!(slice.is_empty());
}
#[test]
fn roundtrip_null_key() {
let r = Record::new(0, None, Bytes::from_static(b"v"));
let expected = r.clone();
let mut buf = BytesMut::new();
r.encode(&mut buf);
let mut slice: &[u8] = &buf;
let decoded = Record::decode(&mut slice).unwrap();
assert_eq!(decoded, expected);
}
#[test]
fn empty_key_is_distinct_from_null_key() {
let with_empty = Record::new(0, Some(Bytes::new()), Bytes::from_static(b"v"));
let with_null = Record::new(0, None, Bytes::from_static(b"v"));
let mut buf_a = BytesMut::new();
let mut buf_b = BytesMut::new();
with_empty.clone().encode(&mut buf_a);
with_null.clone().encode(&mut buf_b);
assert_ne!(buf_a, buf_b);
let mut slice_a: &[u8] = &buf_a;
let mut slice_b: &[u8] = &buf_b;
assert_eq!(Record::decode(&mut slice_a).unwrap(), with_empty);
assert_eq!(Record::decode(&mut slice_b).unwrap(), with_null);
}
#[test]
fn crc_tampering_detected() {
let r = sample();
let mut buf = BytesMut::new();
r.encode(&mut buf);
buf[12] ^= 0xFF;
let mut slice: &[u8] = &buf;
match Record::decode(&mut slice) {
Err(KafkoError::CrcMismatch { .. }) => {}
other => panic!("expected CrcMismatch, got {:?}", other),
}
}
#[test]
fn truncated_input_detected() {
let r = sample();
let mut buf = BytesMut::new();
r.encode(&mut buf);
let truncated = &buf[..buf.len() - 1];
let mut slice: &[u8] = truncated;
match Record::decode(&mut slice) {
Err(KafkoError::Truncated { .. }) => {}
other => panic!("expected Truncated, got {:?}", other),
}
}
#[test]
fn empty_input_truncated() {
let mut slice: &[u8] = &[];
match Record::decode(&mut slice) {
Err(KafkoError::Truncated { .. }) => {}
other => panic!("expected Truncated, got {:?}", other),
}
}
#[test]
fn encode_consumes_self() {
let r = sample();
let mut buf = BytesMut::new();
r.encode(&mut buf);
assert_eq!(buf.len(), 25 + 3 + 5);
}
#[test]
fn lz4_compression_roundtrip() {
let payload = Bytes::from(vec![0xABu8; 1024]);
let r = Record::new(1_700_000_000_000, Some(Bytes::from_static(b"k")), payload.clone());
let mut buf = BytesMut::new();
let actual_size = r.encode_with(&mut buf, Compression::Lz4);
assert!(
actual_size < 1024,
"expected compressed output to be smaller than 1024 bytes, got {}",
actual_size
);
let mut slice: &[u8] = &buf;
let decoded = Record::decode(&mut slice).unwrap();
assert_eq!(decoded.value(), &payload);
assert!(slice.is_empty());
}
#[test]
fn zstd_compression_roundtrip() {
let payload = Bytes::from(vec![0xABu8; 1024]);
let r = Record::new(1_700_000_000_000, Some(Bytes::from_static(b"k")), payload.clone());
let mut buf = BytesMut::new();
let actual_size = r.encode_with(&mut buf, Compression::Zstd);
assert!(
actual_size < 1024,
"expected zstd-compressed output to be smaller than 1024 bytes, got {}",
actual_size
);
let mut slice: &[u8] = &buf;
let decoded = Record::decode(&mut slice).unwrap();
assert_eq!(decoded.value(), &payload);
assert!(slice.is_empty());
}
#[test]
fn lz4_record_decodes_without_caller_knowing_compression() {
let payload = Bytes::from(vec![0xFFu8; 2048]);
let r = Record::new(42, None, payload.clone());
let mut buf = BytesMut::new();
r.encode_with(&mut buf, Compression::Lz4);
let mut slice: &[u8] = &buf;
let decoded = Record::decode(&mut slice).unwrap();
assert_eq!(decoded.value(), &payload);
}
}