pub const ENTRY_SIZE: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PtrmapType {
RootPage,
FreePage,
Overflow1,
Overflow2,
Btree,
}
impl PtrmapType {
pub fn as_u8(self) -> u8 {
match self {
PtrmapType::RootPage => 1,
PtrmapType::FreePage => 2,
PtrmapType::Overflow1 => 3,
PtrmapType::Overflow2 => 4,
PtrmapType::Btree => 5,
}
}
pub fn from_u8(byte: u8) -> Option<Self> {
match byte {
1 => Some(PtrmapType::RootPage),
2 => Some(PtrmapType::FreePage),
3 => Some(PtrmapType::Overflow1),
4 => Some(PtrmapType::Overflow2),
5 => Some(PtrmapType::Btree),
_ => None,
}
}
}
fn entries_per_page(usable_size: u32) -> u32 {
usable_size / ENTRY_SIZE as u32
}
pub fn ptrmap_pageno(usable_size: u32, pgno: u32) -> u32 {
let n = entries_per_page(usable_size);
let cycle = n + 1;
let offset = pgno - 2;
let cycle_index = offset / cycle;
cycle_index * cycle + 2
}
pub fn is_ptrmap_page(usable_size: u32, pgno: u32) -> bool {
if pgno < 2 {
return false;
}
ptrmap_pageno(usable_size, pgno) == pgno
}
pub fn ptrmap_entry_offset(usable_size: u32, pgno: u32) -> usize {
let map = ptrmap_pageno(usable_size, pgno);
let slot = pgno - (map + 1);
slot as usize * ENTRY_SIZE
}
pub fn encode_entry(kind: PtrmapType, parent: u32) -> [u8; ENTRY_SIZE] {
let p = parent.to_be_bytes();
[kind.as_u8(), p[0], p[1], p[2], p[3]]
}
pub fn decode_entry(entry: &[u8; ENTRY_SIZE]) -> Option<(PtrmapType, u32)> {
let kind = PtrmapType::from_u8(entry[0])?;
let parent = u32::from_be_bytes([entry[1], entry[2], entry[3], entry[4]]);
Some((kind, parent))
}
#[cfg(test)]
mod tests {
use super::*;
const USABLE_4K: u32 = 4096;
const N_4K: u32 = 819;
#[test]
fn entries_per_page_matches_spec() {
assert_eq!(entries_per_page(USABLE_4K), N_4K);
assert_eq!(entries_per_page(512), 102); }
#[test]
fn page2_is_first_ptrmap_page() {
assert!(is_ptrmap_page(USABLE_4K, 2));
}
#[test]
fn page1_and_page3_not_ptrmap() {
assert!(!is_ptrmap_page(USABLE_4K, 1));
assert!(!is_ptrmap_page(USABLE_4K, 3));
}
#[test]
fn page3_entry_lives_on_page2_at_offset_0() {
assert_eq!(ptrmap_pageno(USABLE_4K, 3), 2);
assert_eq!(ptrmap_entry_offset(USABLE_4K, 3), 0);
}
#[test]
fn first_ptrmap_covers_run_then_recurs() {
let last_tracked = N_4K + 2;
assert_eq!(ptrmap_pageno(USABLE_4K, last_tracked), 2);
assert_eq!(
ptrmap_entry_offset(USABLE_4K, last_tracked),
(N_4K as usize - 1) * ENTRY_SIZE
);
let next_map = N_4K + 3;
assert!(is_ptrmap_page(USABLE_4K, next_map));
assert_eq!(ptrmap_pageno(USABLE_4K, next_map), next_map);
}
#[test]
fn second_ptrmap_run() {
let map2 = N_4K + 3;
let first = map2 + 1; let last = 2 * N_4K + 3;
assert_eq!(ptrmap_pageno(USABLE_4K, first), map2);
assert_eq!(ptrmap_entry_offset(USABLE_4K, first), 0);
assert_eq!(ptrmap_pageno(USABLE_4K, last), map2);
assert_eq!(
ptrmap_entry_offset(USABLE_4K, last),
(N_4K as usize - 1) * ENTRY_SIZE
);
let map3 = 2 * N_4K + 4;
assert!(is_ptrmap_page(USABLE_4K, map3));
}
#[test]
fn straddling_a_ptrmap_boundary() {
let usable = 20;
assert_eq!(entries_per_page(usable), 4);
assert!(is_ptrmap_page(usable, 2));
for p in 3..=6 {
assert_eq!(ptrmap_pageno(usable, p), 2, "page {p}");
assert!(!is_ptrmap_page(usable, p));
}
assert!(is_ptrmap_page(usable, 7));
for p in 8..=11 {
assert_eq!(ptrmap_pageno(usable, p), 7, "page {p}");
assert!(!is_ptrmap_page(usable, p));
}
assert!(is_ptrmap_page(usable, 12));
assert_eq!(ptrmap_entry_offset(usable, 8), 0);
assert_eq!(ptrmap_entry_offset(usable, 9), ENTRY_SIZE);
assert_eq!(ptrmap_entry_offset(usable, 11), 3 * ENTRY_SIZE);
}
#[test]
fn type_byte_round_trip() {
for kind in [
PtrmapType::RootPage,
PtrmapType::FreePage,
PtrmapType::Overflow1,
PtrmapType::Overflow2,
PtrmapType::Btree,
] {
assert_eq!(PtrmapType::from_u8(kind.as_u8()), Some(kind));
}
}
#[test]
fn type_byte_values_match_sqlite() {
assert_eq!(PtrmapType::RootPage.as_u8(), 1);
assert_eq!(PtrmapType::FreePage.as_u8(), 2);
assert_eq!(PtrmapType::Overflow1.as_u8(), 3);
assert_eq!(PtrmapType::Overflow2.as_u8(), 4);
assert_eq!(PtrmapType::Btree.as_u8(), 5);
}
#[test]
fn unknown_type_byte_rejected() {
assert_eq!(PtrmapType::from_u8(0), None);
assert_eq!(PtrmapType::from_u8(6), None);
assert_eq!(PtrmapType::from_u8(255), None);
}
#[test]
fn entry_encode_decode_round_trip() {
let parents = [
0u32,
1,
2,
255,
256,
65535,
65536,
819,
0x0123_4567,
u32::MAX,
];
for kind in [
PtrmapType::RootPage,
PtrmapType::FreePage,
PtrmapType::Overflow1,
PtrmapType::Overflow2,
PtrmapType::Btree,
] {
for &parent in &parents {
let enc = encode_entry(kind, parent);
assert_eq!(decode_entry(&enc), Some((kind, parent)));
}
}
}
#[test]
fn encode_layout_is_type_then_be_parent() {
let enc = encode_entry(PtrmapType::Overflow1, 0x0102_0304);
assert_eq!(enc, [3, 0x01, 0x02, 0x03, 0x04]);
}
#[test]
fn decode_rejects_bad_type() {
let bad = [9u8, 0, 0, 0, 1];
assert_eq!(decode_entry(&bad), None);
}
}