use wbase::crc::{Crc32Hasher, crc32};
use crate::error::{Error, Result};
pub const RECORD_HEADER_LEN: usize = 8;
const EMPTY_PAYLOAD_CRC: u32 = 0xFFFF_FFFF;
#[inline]
fn payload_crc(payload: &[u8]) -> u32 {
if payload.is_empty() {
EMPTY_PAYLOAD_CRC
} else {
crc32(payload)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct WalFrameHeader {
pub entry_len: u32,
pub crc32: u32,
}
impl WalFrameHeader {
#[inline]
pub const fn new(entry_len: u32, crc32: u32) -> Self {
Self { entry_len, crc32 }
}
#[inline]
pub const fn is_zero(&self) -> bool {
self.entry_len == 0 && self.crc32 == 0
}
#[inline]
pub const fn payload_len(&self) -> usize {
self.entry_len as usize
}
#[inline]
pub fn for_payload(payload: &[u8]) -> Self {
Self {
entry_len: payload.len() as u32,
crc32: payload_crc(payload),
}
}
#[inline]
pub fn for_payload_parts(parts: &[&[u8]]) -> Self {
let total_len: usize = parts.iter().map(|part| part.len()).sum();
if total_len == 0 {
return Self::new(0, EMPTY_PAYLOAD_CRC);
}
let mut hasher = Crc32Hasher::new();
for part in parts {
hasher.update(part);
}
Self {
entry_len: total_len as u32,
crc32: hasher.finalize(),
}
}
#[inline]
pub fn verify(&self, payload: &[u8]) -> Result<()> {
if payload.len() != self.entry_len as usize {
return Err(Error::InvalidRecordHeader);
}
let actual = payload_crc(payload);
if actual != self.crc32 {
return Err(Error::ChecksumMismatch {
expected: self.crc32,
actual,
});
}
Ok(())
}
#[inline]
pub const fn to_bytes(&self) -> [u8; RECORD_HEADER_LEN] {
let packed = (self.entry_len as u64) | ((self.crc32 as u64) << 32);
packed.to_le_bytes()
}
#[inline]
pub const fn from_bytes(src: &[u8; RECORD_HEADER_LEN]) -> Self {
let packed = u64::from_le_bytes(*src);
Self {
entry_len: packed as u32,
crc32: (packed >> 32) as u32,
}
}
#[inline(always)]
pub const fn decode_opt(src: &[u8]) -> Option<Self> {
if let Some((chunk, _)) = src.split_first_chunk::<RECORD_HEADER_LEN>() {
Some(Self::from_bytes(chunk))
} else {
None
}
}
#[inline]
pub fn decode(src: &[u8]) -> Result<Self> {
Self::decode_opt(src).ok_or(Error::InvalidRecordHeader)
}
}
#[cfg(test)]
mod tests {
use super::{EMPTY_PAYLOAD_CRC, RECORD_HEADER_LEN, WalFrameHeader};
#[test]
fn test_record_header_roundtrip() {
let header = WalFrameHeader::new(128, 0x1234_5678);
let bytes = header.to_bytes();
let decoded = WalFrameHeader::decode(&bytes).unwrap();
assert_eq!(decoded, header);
assert_eq!(decoded.payload_len(), 128);
assert!(!decoded.is_zero());
let zero = WalFrameHeader::new(0, 0);
assert!(zero.is_zero());
let empty_payload_header = WalFrameHeader::for_payload(&[]);
assert_eq!(empty_payload_header.payload_len(), 0);
assert!(
!empty_payload_header.is_zero(),
"空有效记录头必须携带非零哨兵 CRC"
);
assert_eq!(empty_payload_header.crc32, EMPTY_PAYLOAD_CRC);
let all_zeros = [0u8; RECORD_HEADER_LEN];
let zero_decoded = WalFrameHeader::decode(&all_zeros).unwrap();
assert!(zero_decoded.is_zero(), "全零头唯一标识 padding 或残缺尾部");
}
#[test]
fn test_record_header_for_payload_and_verify() {
let payload = b"hello aof payload";
let header = WalFrameHeader::for_payload(payload);
assert_eq!(header.payload_len(), payload.len());
assert!(header.verify(payload).is_ok());
let corrupted = b"hello aof payloae";
assert!(header.verify(corrupted).is_err());
assert!(header.verify(&payload[..payload.len() - 1]).is_err());
}
#[test]
fn test_record_header_decode_boundary() {
let short = [0u8; 7];
assert!(WalFrameHeader::decode_opt(&short).is_none());
assert!(WalFrameHeader::decode(&short).is_err());
}
}