use std::fs::File;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::error::DbResult;
pub const TOMBSTONE_BIT: u64 = 1 << 63;
pub const SOFT_DELETE_BIT: u64 = 1 << 62;
pub const FLAGS_MASK: u64 = 0xF000_0000_0000_0000;
pub const SEQUENCE_MASK: u64 = !FLAGS_MASK;
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct EntryHeader {
pub gsn: u64,
pub crc32: u32,
pub value_len: u32,
}
const _: () = assert!(size_of::<EntryHeader>() == 16);
const _: () = assert!(align_of::<EntryHeader>() == 8);
impl EntryHeader {
#[inline]
pub fn is_tombstone(&self) -> bool {
self.gsn & TOMBSTONE_BIT != 0
}
#[inline]
pub fn is_soft_deleted(&self) -> bool {
self.gsn & SOFT_DELETE_BIT != 0
}
#[inline]
pub fn sequence(&self) -> u64 {
self.gsn & SEQUENCE_MASK
}
}
#[inline]
pub(crate) fn guard_gsn(gsn: u64) -> DbResult<()> {
if gsn & FLAGS_MASK != 0 {
return Err(crate::error::DbError::FormatMismatch(
"GSN sequence space exhausted (entered reserved flag bits)".into(),
));
}
Ok(())
}
#[inline]
pub fn make_tombstone_gsn(gsn: u64) -> u64 {
gsn | TOMBSTONE_BIT
}
pub fn compute_crc32(gsn: u64, value_len: u32, key: &[u8], value: &[u8]) -> u32 {
let mut hasher = crc32fast::Hasher::new();
hasher.update(&gsn.to_ne_bytes());
hasher.update(&value_len.to_ne_bytes());
hasher.update(key);
hasher.update(value);
hasher.finalize()
}
#[allow(clippy::type_complexity)]
pub fn check_page_padding_at(
file: &File,
offset: u64,
read_fn: &dyn Fn(&File, u64, usize) -> DbResult<Vec<u8>>,
) -> DbResult<bool> {
const PAGE_SIZE: u64 = 4096;
let page_end = offset.div_ceil(PAGE_SIZE) * PAGE_SIZE;
if page_end == offset {
return Ok(true);
}
let remaining = (page_end - offset) as usize;
match read_fn(file, offset, remaining) {
Ok(b) => Ok(b.iter().all(|&x| x == 0)),
Err(_) => Ok(false),
}
}
#[inline]
pub const fn entry_size(key_len: usize, value_len: u32) -> u64 {
let raw = size_of::<EntryHeader>() + key_len + value_len as usize;
let padded = (raw + 7) & !7;
padded as u64
}
pub fn serialize_entry(gsn: u64, key: &[u8], value: &[u8], tombstone: bool) -> Vec<u8> {
let actual_gsn = if tombstone {
make_tombstone_gsn(gsn)
} else {
gsn
};
let value_len = value.len() as u32;
let crc = compute_crc32(actual_gsn, value_len, key, value);
let total = entry_size(key.len(), value_len) as usize;
let mut buf = vec![0u8; total];
let header = EntryHeader {
gsn: actual_gsn,
crc32: crc,
value_len,
};
buf[..16].copy_from_slice(header.as_bytes());
buf[16..16 + key.len()].copy_from_slice(key);
buf[16 + key.len()..16 + key.len() + value.len()].copy_from_slice(value);
buf
}
#[cfg(test)]
mod tests {
use super::*;
use zerocopy::FromBytes;
#[test]
fn test_tombstone_encoding() {
let gsn = 123u64;
let tombstone_gsn = make_tombstone_gsn(gsn);
assert_ne!(tombstone_gsn, gsn);
assert!(tombstone_gsn & TOMBSTONE_BIT != 0);
let header = EntryHeader {
gsn: tombstone_gsn,
crc32: 0,
value_len: 0,
};
assert!(header.is_tombstone());
assert_eq!(header.sequence(), gsn);
}
#[test]
fn test_sequence_preserves_value() {
let header = EntryHeader {
gsn: 42,
crc32: 0,
value_len: 0,
};
assert_eq!(header.sequence(), 42);
assert!(!header.is_tombstone());
}
#[test]
fn test_entry_size_alignment() {
for key_len in 0..64 {
for val_len in 0..64u32 {
let size = entry_size(key_len, val_len);
assert_eq!(size % 8, 0, "key_len={key_len}, val_len={val_len}");
}
}
}
#[test]
fn test_entry_size_exact() {
assert_eq!(entry_size(8, 0), 24);
assert_eq!(entry_size(8, 8), 32);
assert_eq!(entry_size(16, 1), 40);
}
#[test]
fn test_serialize_deserialize() {
let key = b"test_key";
let value = b"test_value";
let gsn = 42u64;
let buf = serialize_entry(gsn, key, value, false);
let header = EntryHeader::read_from_bytes(&buf[..16]).expect("failed to parse header");
assert_eq!(header.gsn, 42);
assert_eq!(header.value_len, value.len() as u32);
let expected_crc = compute_crc32(gsn, value.len() as u32, key, value);
assert_eq!(header.crc32, expected_crc);
}
#[test]
fn test_serialize_tombstone() {
let key = b"tombkey";
let value = b"";
let gsn = 99u64;
let buf = serialize_entry(gsn, key, value, true);
let header = EntryHeader::read_from_bytes(&buf[..16]).expect("failed to parse header");
assert!(header.is_tombstone());
assert_eq!(header.sequence(), gsn);
}
#[test]
fn test_crc_detects_corruption() {
let key = b"mykey";
let value = b"myvalue";
let gsn = 7u64;
let mut buf = serialize_entry(gsn, key, value, false);
let header = EntryHeader::read_from_bytes(&buf[..16]).expect("failed to parse header");
let original_crc = header.crc32;
let value_offset = 16 + key.len();
buf[value_offset] ^= 0xFF;
let corrupted_value = &buf[value_offset..value_offset + value.len()];
let new_crc = compute_crc32(header.gsn, header.value_len, key, corrupted_value);
assert_ne!(new_crc, original_crc);
}
#[test]
fn flag_masks_partition_u64() {
assert_eq!(FLAGS_MASK | SEQUENCE_MASK, u64::MAX);
assert_eq!(FLAGS_MASK & SEQUENCE_MASK, 0);
assert_eq!(SEQUENCE_MASK.count_ones(), 60);
assert_eq!(TOMBSTONE_BIT, 1 << 63);
assert_eq!(SOFT_DELETE_BIT, 1 << 62);
}
#[test]
fn sequence_strips_all_flag_bits() {
let gsn = 42u64 | TOMBSTONE_BIT | SOFT_DELETE_BIT;
let h = EntryHeader {
gsn,
crc32: 0,
value_len: 0,
};
assert_eq!(h.sequence(), 42);
assert!(h.is_tombstone());
assert!(h.is_soft_deleted());
}
#[test]
fn soft_deleted_future_record_reads_live_sequence() {
let gsn = 1000u64 | SOFT_DELETE_BIT;
let h = EntryHeader {
gsn,
crc32: 0,
value_len: 0,
};
assert_eq!(h.sequence(), 1000);
assert!(!h.is_tombstone());
assert!(h.is_soft_deleted());
}
#[test]
fn guard_gsn_accepts_max_sequence_rejects_flag_bits() {
assert!(guard_gsn(SEQUENCE_MASK).is_ok());
assert!(guard_gsn(0).is_ok());
assert!(guard_gsn(SEQUENCE_MASK + 1).is_err()); assert!(guard_gsn(5 | SOFT_DELETE_BIT).is_err());
assert!(guard_gsn(5 | TOMBSTONE_BIT).is_err());
}
}