use xxhash_rust::xxh3::{Xxh3, xxh3_64};
use super::LogSeq;
pub const RECORD_HEADER_LEN: usize = 14;
pub const RECORD_CHECKSUM_LEN: usize = 8;
pub const MIN_RECORD_LEN: usize = RECORD_HEADER_LEN + RECORD_CHECKSUM_LEN;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WalRecord<'a> {
pub seq: LogSeq,
pub op: u16,
pub payload: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecodedRecord<'a> {
pub record: WalRecord<'a>,
pub encoded_len: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RecordEncodeError {
PayloadTooLarge(usize),
}
impl std::fmt::Display for RecordEncodeError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PayloadTooLarge(length) => {
write!(formatter, "WAL payload length {length} exceeds u32")
}
}
}
}
impl std::error::Error for RecordEncodeError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RecordError {
HeaderTruncated {
needed: usize,
available: usize,
},
LengthOverflow {
payload_length: u32,
},
BodyTruncated {
payload_length: u32,
needed: usize,
available: usize,
},
ChecksumMismatch {
expected: u64,
actual: u64,
record_length: usize,
},
}
impl RecordError {
pub(crate) const fn framed_length(self) -> Option<usize> {
match self {
Self::ChecksumMismatch { record_length, .. } => Some(record_length),
Self::HeaderTruncated { .. }
| Self::LengthOverflow { .. }
| Self::BodyTruncated { .. } => None,
}
}
}
impl std::fmt::Display for RecordError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HeaderTruncated { needed, available } => {
write!(
formatter,
"WAL header needs {needed} bytes, got {available}"
)
}
Self::LengthOverflow { payload_length } => {
write!(
formatter,
"WAL payload length {payload_length} overflows framing"
)
}
Self::BodyTruncated {
payload_length,
needed,
available,
} => write!(
formatter,
"WAL payload length {payload_length} needs {needed} framed bytes, got {available}"
),
Self::ChecksumMismatch {
expected, actual, ..
} => write!(
formatter,
"WAL checksum expected {expected:#018x}, computed {actual:#018x}"
),
}
}
}
impl std::error::Error for RecordError {}
pub fn encode_record(record: WalRecord<'_>) -> Result<Vec<u8>, RecordEncodeError> {
let encoded_len = encoded_record_len(record.payload.len())?;
let mut encoded = Vec::with_capacity(encoded_len);
encode_record_into(record, &mut encoded)?;
Ok(encoded)
}
pub(crate) fn encoded_record_len(payload_len: usize) -> Result<usize, RecordEncodeError> {
u32::try_from(payload_len).map_err(|_| RecordEncodeError::PayloadTooLarge(payload_len))?;
Ok(MIN_RECORD_LEN.saturating_add(payload_len))
}
pub(crate) fn encode_record_into(
record: WalRecord<'_>,
encoded: &mut Vec<u8>,
) -> Result<(), RecordEncodeError> {
encoded.clear();
append_record_into(record, encoded)
}
pub(crate) fn append_record_into(
record: WalRecord<'_>,
encoded: &mut Vec<u8>,
) -> Result<(), RecordEncodeError> {
let payload_length = u32::try_from(record.payload.len())
.map_err(|_| RecordEncodeError::PayloadTooLarge(record.payload.len()))?;
let payload_length_bytes = payload_length.to_le_bytes();
let sequence_bytes = record.seq.get().to_le_bytes();
let operation_bytes = record.op.to_le_bytes();
let mut checksum = Xxh3::new();
checksum.update(&payload_length_bytes);
checksum.update(&sequence_bytes);
checksum.update(&operation_bytes);
checksum.update(record.payload);
encoded.extend_from_slice(&payload_length_bytes);
encoded.extend_from_slice(&sequence_bytes);
encoded.extend_from_slice(&operation_bytes);
encoded.extend_from_slice(record.payload);
encoded.extend_from_slice(&checksum.digest().to_le_bytes());
Ok(())
}
pub fn decode_record(bytes: &[u8]) -> Result<DecodedRecord<'_>, RecordError> {
if bytes.len() < RECORD_HEADER_LEN {
return Err(RecordError::HeaderTruncated {
needed: RECORD_HEADER_LEN,
available: bytes.len(),
});
}
let payload_length = read_u32(bytes, 0)?;
let payload_length_usize = usize::try_from(payload_length)
.map_err(|_| RecordError::LengthOverflow { payload_length })?;
let encoded_len = MIN_RECORD_LEN
.checked_add(payload_length_usize)
.ok_or(RecordError::LengthOverflow { payload_length })?;
if bytes.len() < encoded_len {
return Err(RecordError::BodyTruncated {
payload_length,
needed: encoded_len,
available: bytes.len(),
});
}
let payload_end = RECORD_HEADER_LEN.saturating_add(payload_length_usize);
let payload = bytes
.get(RECORD_HEADER_LEN..payload_end)
.ok_or(RecordError::BodyTruncated {
payload_length,
needed: encoded_len,
available: bytes.len(),
})?;
let expected = read_u64(bytes, payload_end)?;
let checksummed = bytes.get(..payload_end).ok_or(RecordError::BodyTruncated {
payload_length,
needed: encoded_len,
available: bytes.len(),
})?;
let actual = xxh3_64(checksummed);
if actual != expected {
return Err(RecordError::ChecksumMismatch {
expected,
actual,
record_length: encoded_len,
});
}
Ok(DecodedRecord {
record: WalRecord {
seq: LogSeq::new(read_u64(bytes, 4)?),
op: read_u16(bytes, 12)?,
payload,
},
encoded_len,
})
}
fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, RecordError> {
let raw = bytes
.get(offset..offset.saturating_add(2))
.and_then(|value| <[u8; 2]>::try_from(value).ok())
.ok_or(RecordError::HeaderTruncated {
needed: RECORD_HEADER_LEN,
available: bytes.len(),
})?;
Ok(u16::from_le_bytes(raw))
}
fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, RecordError> {
let raw = bytes
.get(offset..offset.saturating_add(4))
.and_then(|value| <[u8; 4]>::try_from(value).ok())
.ok_or(RecordError::HeaderTruncated {
needed: RECORD_HEADER_LEN,
available: bytes.len(),
})?;
Ok(u32::from_le_bytes(raw))
}
fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, RecordError> {
let raw = bytes
.get(offset..offset.saturating_add(8))
.and_then(|value| <[u8; 8]>::try_from(value).ok())
.ok_or(RecordError::BodyTruncated {
payload_length: 0,
needed: offset.saturating_add(8),
available: bytes.len(),
})?;
Ok(u64::from_le_bytes(raw))
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::{WalRecord, encode_record_into, encoded_record_len};
use crate::wal::LogSeq;
#[test]
fn caller_reserved_encoding_keeps_its_allocation() {
let payload = [0x5a; 1_024];
let encoded_len = encoded_record_len(payload.len()).expect("valid payload length");
let mut encoded = Vec::with_capacity(encoded_len);
let allocation = encoded.as_ptr();
let capacity = encoded.capacity();
encode_record_into(
WalRecord {
seq: LogSeq::new(7),
op: 3,
payload: &payload,
},
&mut encoded,
)
.expect("encode into reserved allocation");
assert_eq!(encoded.as_ptr(), allocation);
assert_eq!(encoded.capacity(), capacity);
assert_eq!(encoded.len(), encoded_len);
}
}