#![forbid(unsafe_code)]
use crate::pager::checksum::crc32c_append;
use crate::pager::page::PAGE_SIZE;
pub const WAL_MAGIC: [u8; 4] = *b"OBJW";
pub const WAL_HEADER_SIZE: usize = 64;
pub const FRAME_HEADER_SIZE: usize = 64;
pub const FRAME_SIZE: usize = FRAME_HEADER_SIZE + PAGE_SIZE;
pub const FRAME_AEAD_SUFFIX_SIZE: usize = 24 + 16;
pub const FRAME_SIZE_ENCRYPTED: usize = FRAME_SIZE + FRAME_AEAD_SUFFIX_SIZE;
#[must_use]
pub const fn frame_size_for(encrypted: bool) -> usize {
if encrypted {
FRAME_SIZE_ENCRYPTED
} else {
FRAME_SIZE
}
}
const OFF_PAGE_ID: usize = 0;
const OFF_LSN: usize = 8;
const OFF_SALT: usize = 16;
const OFF_FLAGS: usize = 20;
const OFF_CRC: usize = 60;
const FLAG_COMMIT: u8 = 0x01;
#[derive(Debug, Clone, Copy)]
pub struct FrameHeader {
pub page_id: u64,
pub lsn: u64,
pub salt: u32,
pub commit: bool,
}
pub fn encode_frame_header(header: &FrameHeader, buf: &mut [u8]) {
debug_assert_eq!(buf.len(), FRAME_SIZE, "frame buffer must be FRAME_SIZE");
for b in buf.iter_mut().take(FRAME_HEADER_SIZE) {
*b = 0;
}
buf[OFF_PAGE_ID..OFF_PAGE_ID + 8].copy_from_slice(&header.page_id.to_le_bytes());
buf[OFF_LSN..OFF_LSN + 8].copy_from_slice(&header.lsn.to_le_bytes());
buf[OFF_SALT..OFF_SALT + 4].copy_from_slice(&header.salt.to_le_bytes());
buf[OFF_FLAGS] = if header.commit { FLAG_COMMIT } else { 0 };
let crc = compute_frame_crc(buf);
buf[OFF_CRC..OFF_CRC + 4].copy_from_slice(&crc.to_le_bytes());
}
#[must_use]
pub fn decode_frame_header(buf: &[u8], expected_salt: u32) -> Option<FrameHeader> {
match decode_frame_header_classified(buf, expected_salt) {
FrameDecode::Ok(header) => Some(header),
FrameDecode::SaltMismatch | FrameDecode::CrcInvalid | FrameDecode::Malformed => None,
}
}
#[derive(Debug)]
pub enum FrameDecode {
Ok(FrameHeader),
SaltMismatch,
CrcInvalid,
Malformed,
}
#[must_use]
pub fn decode_frame_header_classified(buf: &[u8], expected_salt: u32) -> FrameDecode {
if buf.len() != FRAME_SIZE {
return FrameDecode::Malformed;
}
let salt = u32::from_le_bytes([
buf[OFF_SALT],
buf[OFF_SALT + 1],
buf[OFF_SALT + 2],
buf[OFF_SALT + 3],
]);
if salt != expected_salt {
return FrameDecode::SaltMismatch;
}
let stored_crc = u32::from_le_bytes([
buf[OFF_CRC],
buf[OFF_CRC + 1],
buf[OFF_CRC + 2],
buf[OFF_CRC + 3],
]);
let computed = compute_frame_crc(buf);
if stored_crc != computed {
return FrameDecode::CrcInvalid;
}
let flags = buf[OFF_FLAGS];
if flags & !FLAG_COMMIT != 0 {
return FrameDecode::Malformed;
}
let page_id = u64::from_le_bytes([
buf[OFF_PAGE_ID],
buf[OFF_PAGE_ID + 1],
buf[OFF_PAGE_ID + 2],
buf[OFF_PAGE_ID + 3],
buf[OFF_PAGE_ID + 4],
buf[OFF_PAGE_ID + 5],
buf[OFF_PAGE_ID + 6],
buf[OFF_PAGE_ID + 7],
]);
let lsn = u64::from_le_bytes([
buf[OFF_LSN],
buf[OFF_LSN + 1],
buf[OFF_LSN + 2],
buf[OFF_LSN + 3],
buf[OFF_LSN + 4],
buf[OFF_LSN + 5],
buf[OFF_LSN + 6],
buf[OFF_LSN + 7],
]);
FrameDecode::Ok(FrameHeader {
page_id,
lsn,
salt,
commit: flags & FLAG_COMMIT != 0,
})
}
#[must_use]
pub fn frame_offset(frame_index: u64, frame_size: usize) -> u64 {
frame_index
.checked_mul(frame_size as u64)
.and_then(|product| product.checked_add(WAL_HEADER_SIZE as u64))
.unwrap_or(u64::MAX)
}
fn compute_frame_crc(buf: &[u8]) -> u32 {
debug_assert_eq!(buf.len(), FRAME_SIZE);
debug_assert_eq!(OFF_CRC + 4, FRAME_HEADER_SIZE, "CRC field ends the header");
let crc = crc32c_append(0, &buf[..OFF_CRC]);
let crc = crc32c_append(crc, &[0u8; 4]);
crc32c_append(crc, &buf[OFF_CRC + 4..])
}
#[cfg(test)]
mod tests {
use super::{
compute_frame_crc, decode_frame_header, encode_frame_header, FrameHeader,
FRAME_HEADER_SIZE, FRAME_SIZE, OFF_CRC,
};
use crate::pager::checksum::crc32c;
fn compute_frame_crc_reference(buf: &[u8]) -> u32 {
assert_eq!(buf.len(), FRAME_SIZE);
let mut linear = [0u8; FRAME_SIZE];
linear[..FRAME_HEADER_SIZE].copy_from_slice(&buf[..FRAME_HEADER_SIZE]);
for b in &mut linear[OFF_CRC..OFF_CRC + 4] {
*b = 0;
}
linear[FRAME_HEADER_SIZE..].copy_from_slice(&buf[FRAME_HEADER_SIZE..]);
crc32c(&linear)
}
#[test]
fn crc_byte_identical_to_memcpy_reference() {
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for case in 0..256u32 {
let mut buf = [0u8; FRAME_SIZE];
for b in &mut buf {
*b = u8::try_from(next() & 0xFF).expect("masked to a byte");
}
let got = compute_frame_crc(&buf);
let want = compute_frame_crc_reference(&buf);
assert_eq!(
got, want,
"case {case}: crc32c_append result diverged from memcpy reference"
);
}
let zero = [0u8; FRAME_SIZE];
assert_eq!(compute_frame_crc(&zero), compute_frame_crc_reference(&zero));
let ones = [0xFFu8; FRAME_SIZE];
assert_eq!(compute_frame_crc(&ones), compute_frame_crc_reference(&ones));
}
#[test]
fn crc_ignores_stale_crc_field_bytes() {
let mut a = [0xABu8; FRAME_SIZE];
for (i, b) in a.iter_mut().enumerate() {
*b = u8::try_from(i & 0xFF).expect("masked");
}
let mut b = a;
a[OFF_CRC..OFF_CRC + 4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
b[OFF_CRC..OFF_CRC + 4].copy_from_slice(&0u32.to_le_bytes());
assert_eq!(compute_frame_crc(&a), compute_frame_crc(&b));
assert_eq!(compute_frame_crc(&a), compute_frame_crc_reference(&a));
}
#[test]
fn round_trip_basic_frame() {
let header = FrameHeader {
page_id: 7,
lsn: 42,
salt: 0xDEAD_BEEF,
commit: true,
};
let mut buf = vec![0u8; FRAME_SIZE];
for (i, b) in buf.iter_mut().enumerate().skip(64).take(128) {
*b = u8::try_from(i & 0xFF).expect("masked");
}
encode_frame_header(&header, &mut buf);
let decoded = decode_frame_header(&buf, 0xDEAD_BEEF).expect("decode");
assert_eq!(decoded.page_id, 7);
assert_eq!(decoded.lsn, 42);
assert_eq!(decoded.salt, 0xDEAD_BEEF);
assert!(decoded.commit);
}
#[test]
fn salt_mismatch_yields_tail() {
let header = FrameHeader {
page_id: 1,
lsn: 1,
salt: 1,
commit: true,
};
let mut buf = vec![0u8; FRAME_SIZE];
encode_frame_header(&header, &mut buf);
assert!(decode_frame_header(&buf, 2).is_none());
}
#[test]
fn flipped_body_invalidates_crc() {
let header = FrameHeader {
page_id: 1,
lsn: 1,
salt: 1,
commit: false,
};
let mut buf = vec![0u8; FRAME_SIZE];
buf[64 + 50] = 0xAA;
encode_frame_header(&header, &mut buf);
assert!(decode_frame_header(&buf, 1).is_some());
buf[64 + 50] ^= 0x01;
assert!(decode_frame_header(&buf, 1).is_none());
}
#[test]
fn unknown_flag_bits_are_tail() {
let header = FrameHeader {
page_id: 1,
lsn: 1,
salt: 1,
commit: true,
};
let mut buf = vec![0u8; FRAME_SIZE];
encode_frame_header(&header, &mut buf);
buf[20] = 0x80;
let crc = super::compute_frame_crc(&buf);
buf[60..64].copy_from_slice(&crc.to_le_bytes());
assert!(decode_frame_header(&buf, 1).is_none());
}
}