use crate::storage::page::{PageId, PAGE_SIZE};
use crate::{Error, Result};
const HEADER_CRC_COVERAGE_END: usize = 100;
const HEADER_CRC_OFFSET: usize = 100;
const PAGE_FILE_MAGIC: &[u8; 8] = b"EMDBPAGE";
pub(crate) const PAGE_FORMAT_VERSION: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PageFileHeader {
pub(crate) flags: u32,
pub(crate) page_size: u32,
pub(crate) created_at: u64,
pub(crate) last_tx_id: u64,
pub(crate) page_count: u64,
pub(crate) root_page_id: PageId,
pub(crate) free_list_head: PageId,
pub(crate) value_alloc_head: PageId,
}
impl Default for PageFileHeader {
fn default() -> Self {
Self {
flags: 0,
page_size: PAGE_SIZE as u32,
created_at: 0,
last_tx_id: 0,
page_count: 1,
root_page_id: PageId::new(0),
free_list_head: PageId::new(0),
value_alloc_head: PageId::new(1),
}
}
}
impl PageFileHeader {
#[must_use]
pub(crate) fn encode(self) -> [u8; PAGE_SIZE] {
let mut bytes = [0_u8; PAGE_SIZE];
bytes[0..8].copy_from_slice(PAGE_FILE_MAGIC);
bytes[8..12].copy_from_slice(&PAGE_FORMAT_VERSION.to_le_bytes());
bytes[12..16].copy_from_slice(&self.flags.to_le_bytes());
bytes[16..20].copy_from_slice(&self.page_size.to_le_bytes());
bytes[20..28].copy_from_slice(&self.created_at.to_le_bytes());
bytes[28..36].copy_from_slice(&self.last_tx_id.to_le_bytes());
bytes[36..44].copy_from_slice(&self.page_count.to_le_bytes());
bytes[44..52].copy_from_slice(&self.root_page_id.get().to_le_bytes());
bytes[52..60].copy_from_slice(&self.free_list_head.get().to_le_bytes());
bytes[60..68].copy_from_slice(&self.value_alloc_head.get().to_le_bytes());
let crc = header_crc(&bytes);
bytes[HEADER_CRC_OFFSET..HEADER_CRC_OFFSET + 4].copy_from_slice(&crc.to_le_bytes());
bytes
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() < PAGE_SIZE {
return Err(Error::Corrupted {
offset: 0,
reason: "page header truncated",
});
}
if &bytes[0..8] != PAGE_FILE_MAGIC {
return Err(Error::MagicMismatch);
}
let format_ver = read_u32(bytes, 8);
if format_ver != PAGE_FORMAT_VERSION {
return Err(Error::VersionMismatch {
found: format_ver,
expected: PAGE_FORMAT_VERSION,
});
}
let expected_crc = read_u32(bytes, HEADER_CRC_OFFSET);
let actual_crc = header_crc_prefix(bytes);
if expected_crc != actual_crc {
return Err(Error::Corrupted {
offset: HEADER_CRC_OFFSET as u64,
reason: "page header crc mismatch",
});
}
let page_size = read_u32(bytes, 16);
if page_size != PAGE_SIZE as u32 {
return Err(Error::Corrupted {
offset: 16,
reason: "page size mismatch",
});
}
Ok(Self {
flags: read_u32(bytes, 12),
page_size,
created_at: read_u64(bytes, 20),
last_tx_id: read_u64(bytes, 28),
page_count: read_u64(bytes, 36),
root_page_id: PageId::new(read_u64(bytes, 44)),
free_list_head: PageId::new(read_u64(bytes, 52)),
value_alloc_head: PageId::new(read_u64(bytes, 60)),
})
}
}
fn header_crc(bytes: &[u8; PAGE_SIZE]) -> u32 {
header_crc_prefix(bytes)
}
fn header_crc_prefix(bytes: &[u8]) -> u32 {
let mut hasher = crc32fast::Hasher::new();
hasher.update(&bytes[0..HEADER_CRC_COVERAGE_END]);
hasher.finalize()
}
fn read_u32(bytes: &[u8], offset: usize) -> u32 {
let mut raw = [0_u8; 4];
raw.copy_from_slice(&bytes[offset..offset + 4]);
u32::from_le_bytes(raw)
}
fn read_u64(bytes: &[u8], offset: usize) -> u64 {
let mut raw = [0_u8; 8];
raw.copy_from_slice(&bytes[offset..offset + 8]);
u64::from_le_bytes(raw)
}
#[cfg(test)]
mod tests {
use super::{PageFileHeader, PAGE_FORMAT_VERSION};
use crate::storage::page::{PageId, PAGE_SIZE};
use crate::Error;
#[test]
fn test_write_fresh_header_and_read_back() {
let header = PageFileHeader {
flags: 3,
page_size: PAGE_SIZE as u32,
created_at: 11,
last_tx_id: 19,
page_count: 23,
root_page_id: PageId::new(29),
free_list_head: PageId::new(31),
value_alloc_head: PageId::new(37),
};
let encoded = header.encode();
let decoded = PageFileHeader::decode(&encoded);
assert!(matches!(decoded, Ok(value) if value == header));
}
#[test]
fn test_mutate_header_and_reread() {
let mut header = PageFileHeader::default();
let encoded = header.encode();
let decoded = PageFileHeader::decode(&encoded);
assert!(matches!(decoded, Ok(value) if value == header));
header.last_tx_id = 77;
header.page_count = 101;
header.root_page_id = PageId::new(5);
let encoded = header.encode();
let decoded = PageFileHeader::decode(&encoded);
assert!(matches!(decoded, Ok(value) if value == header));
}
#[test]
fn test_header_crc_validation_rejects_corruption() {
let header = PageFileHeader::default();
let mut encoded = header.encode();
encoded[20] ^= 0xFF;
let decoded = PageFileHeader::decode(&encoded);
assert!(matches!(
decoded,
Err(Error::Corrupted {
reason: "page header crc mismatch",
..
})
));
}
#[test]
fn test_header_version_validation_rejects_mismatch() {
let header = PageFileHeader::default();
let mut encoded = header.encode();
let wrong_version = (PAGE_FORMAT_VERSION + 1).to_le_bytes();
encoded[8..12].copy_from_slice(&wrong_version);
let crc = crc32fast::hash(&encoded[0..100]);
encoded[100..104].copy_from_slice(&crc.to_le_bytes());
let decoded = PageFileHeader::decode(&encoded);
assert!(matches!(
decoded,
Err(Error::VersionMismatch {
found,
expected,
}) if found == PAGE_FORMAT_VERSION + 1 && expected == PAGE_FORMAT_VERSION
));
}
#[test]
fn test_header_magic_validation_rejects_non_page_file() {
let mut encoded = PageFileHeader::default().encode();
encoded[0] = b'X';
let decoded = PageFileHeader::decode(&encoded);
assert!(matches!(decoded, Err(Error::MagicMismatch)));
}
}