use fsqlite_error::{FrankenError, Result};
use fsqlite_types::PageNumber;
use fsqlite_types::limits::MAX_PAGE_COUNT;
#[must_use]
pub const fn max_leaf_entries(usable_size: u32) -> u32 {
(usable_size / 4).saturating_sub(2)
}
#[derive(Debug, Clone)]
pub struct FreelistTrunk {
pub next_trunk: Option<PageNumber>,
pub leaf_pages: Vec<PageNumber>,
}
impl FreelistTrunk {
pub fn parse(page: &[u8]) -> Result<Self> {
if page.len() < 8 {
return Err(FrankenError::DatabaseCorrupt {
detail: "freelist trunk page too small".to_owned(),
});
}
let next_raw = u32::from_be_bytes([page[0], page[1], page[2], page[3]]);
let next_trunk = PageNumber::new(next_raw);
let leaf_count = u32::from_be_bytes([page[4], page[5], page[6], page[7]]);
#[allow(clippy::cast_possible_truncation)]
let max_entries = (page.len() as u32 / 4).saturating_sub(2);
if leaf_count > max_entries {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"freelist trunk claims {} leaf pages but page can hold at most {}",
leaf_count, max_entries
),
});
}
let mut leaf_pages = Vec::with_capacity(leaf_count as usize);
for i in 0..leaf_count as usize {
let offset = 8 + i * 4;
let pgno = u32::from_be_bytes([
page[offset],
page[offset + 1],
page[offset + 2],
page[offset + 3],
]);
if let Some(pn) = PageNumber::new(pgno) {
leaf_pages.push(pn);
}
}
Ok(Self {
next_trunk,
leaf_pages,
})
}
#[allow(clippy::cast_possible_truncation)]
pub fn write(&self, page: &mut [u8]) {
if page.len() < 8 {
return;
}
let next = self.next_trunk.map_or(0u32, PageNumber::get);
page[0..4].copy_from_slice(&next.to_be_bytes());
let max_entries = (page.len() as u32 / 4).saturating_sub(2);
let count = (self.leaf_pages.len() as u32).min(max_entries);
page[4..8].copy_from_slice(&count.to_be_bytes());
for (i, &pgno) in self.leaf_pages.iter().take(count as usize).enumerate() {
let offset = 8 + i * 4;
page[offset..offset + 4].copy_from_slice(&pgno.get().to_be_bytes());
}
}
}
#[derive(Debug, Clone)]
pub struct Freelist {
free_pages: Vec<PageNumber>,
db_page_count: u32,
page_size: u32,
}
impl Freelist {
#[must_use]
pub fn new(db_page_count: u32, page_size: u32) -> Self {
Self {
free_pages: Vec::new(),
db_page_count,
page_size,
}
}
#[must_use]
pub fn with_pages(pages: Vec<PageNumber>, db_page_count: u32, page_size: u32) -> Self {
Self {
free_pages: pages,
db_page_count,
page_size,
}
}
#[must_use]
pub fn free_count(&self) -> usize {
self.free_pages.len()
}
pub fn allocate(&mut self) -> Result<PageNumber> {
while let Some(pgno) = self.free_pages.pop() {
if pgno.get() <= self.db_page_count {
return Ok(pgno);
}
}
if self.db_page_count >= MAX_PAGE_COUNT {
return Err(FrankenError::DatabaseFull);
}
let mut next = self.db_page_count + 1;
let pending_byte_page = (0x4000_0000 / self.page_size) + 1;
if next == pending_byte_page {
if next >= MAX_PAGE_COUNT {
return Err(FrankenError::DatabaseFull);
}
next += 1;
}
self.db_page_count = next;
PageNumber::new(self.db_page_count).ok_or(FrankenError::DatabaseFull)
}
pub fn deallocate(&mut self, page: PageNumber) {
if page.get() <= self.db_page_count {
self.free_pages.push(page);
}
}
#[must_use]
pub fn db_page_count(&self) -> u32 {
self.db_page_count
}
pub fn free_pages(&self) -> &[PageNumber] {
&self.free_pages
}
}
pub const PTRMAP_ENTRY_SIZE_BYTES: u32 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PtrMapType {
RootPage = 1,
FreePage = 2,
Overflow1 = 3,
Overflow2 = 4,
Btree = 5,
}
impl PtrMapType {
fn from_code(code: u8) -> Result<Self> {
match code {
1 => Ok(Self::RootPage),
2 => Ok(Self::FreePage),
3 => Ok(Self::Overflow1),
4 => Ok(Self::Overflow2),
5 => Ok(Self::Btree),
_ => Err(FrankenError::DatabaseCorrupt {
detail: format!("invalid pointer-map type code: {code}"),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PtrMapEntry {
pub kind: PtrMapType,
pub parent: Option<PageNumber>,
}
impl PtrMapEntry {
pub fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() < PTRMAP_ENTRY_SIZE_BYTES as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: "pointer-map entry too small".to_owned(),
});
}
let kind = PtrMapType::from_code(bytes[0])?;
let parent_raw = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
let parent = PageNumber::new(parent_raw);
match kind {
PtrMapType::RootPage | PtrMapType::FreePage => {
if parent_raw != 0 {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"pointer-map type {:?} must have parent 0, got {}",
kind, parent_raw
),
});
}
}
PtrMapType::Overflow1 | PtrMapType::Overflow2 | PtrMapType::Btree => {
if parent.is_none() {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"pointer-map type {:?} requires non-zero parent page",
kind
),
});
}
}
}
Ok(Self { kind, parent })
}
#[must_use]
pub fn encode(self) -> [u8; PTRMAP_ENTRY_SIZE_BYTES as usize] {
let mut out = [0u8; PTRMAP_ENTRY_SIZE_BYTES as usize];
out[0] = self.kind as u8;
let parent = self.parent.map_or(0u32, PageNumber::get);
out[1..5].copy_from_slice(&parent.to_be_bytes());
out
}
}
const PENDING_BYTE_OFFSET: u32 = 0x4000_0000;
#[must_use]
pub const fn ptrmap_entries_per_page(usable_size: u32) -> u32 {
usable_size / PTRMAP_ENTRY_SIZE_BYTES
}
#[must_use]
pub const fn ptrmap_group_size(usable_size: u32) -> u32 {
ptrmap_entries_per_page(usable_size) + 1
}
#[must_use]
const fn compute_ptrmap_page(pgno: u32, usable_size: u32, page_size: u32) -> u32 {
if pgno < 2 {
return 0;
}
let group = ptrmap_group_size(usable_size);
if group == 0 {
return 0;
}
let i_ptr_map = (pgno - 2) / group;
let mut ret = (i_ptr_map * group) + 2;
let pending_byte_page = (PENDING_BYTE_OFFSET / page_size) + 1;
if ret == pending_byte_page {
ret += 1;
}
ret
}
#[must_use]
pub const fn is_ptrmap_page(pgno: PageNumber, usable_size: u32, page_size: u32) -> bool {
let raw = pgno.get();
if raw < 2 {
return false;
}
compute_ptrmap_page(raw, usable_size, page_size) == raw
}
#[must_use]
pub const fn ptrmap_page_for(
pgno: PageNumber,
usable_size: u32,
page_size: u32,
) -> Option<PageNumber> {
if is_ptrmap_page(pgno, usable_size, page_size) {
return None;
}
let raw = pgno.get();
if raw < 3 {
return None;
}
let ptrmap = compute_ptrmap_page(raw, usable_size, page_size);
if ptrmap == 0 {
return None;
}
PageNumber::new(ptrmap)
}
#[must_use]
pub const fn ptrmap_entry_offset(
pgno: PageNumber,
usable_size: u32,
page_size: u32,
) -> Option<u32> {
let pending_byte_page = (PENDING_BYTE_OFFSET / page_size) + 1;
if pgno.get() == pending_byte_page {
return None;
}
let Some(ptrmap_page) = ptrmap_page_for(pgno, usable_size, page_size) else {
return None;
};
if pgno.get() <= ptrmap_page.get() {
return None;
}
let index = pgno.get() - ptrmap_page.get() - 1;
Some(index * PTRMAP_ENTRY_SIZE_BYTES)
}
pub fn read_freelist<F>(
first_trunk: Option<PageNumber>,
read_page: &mut F,
) -> Result<Vec<PageNumber>>
where
F: FnMut(PageNumber) -> Result<Vec<u8>>,
{
let mut all_pages = Vec::new();
let mut current = first_trunk;
let mut visited = 0usize;
while let Some(trunk_pgno) = current {
visited += 1;
if visited > 1_000_000 {
return Err(FrankenError::DatabaseCorrupt {
detail: "freelist trunk chain too long (possible cycle)".to_owned(),
});
}
let page_data = read_page(trunk_pgno)?;
let trunk = FreelistTrunk::parse(&page_data)?;
all_pages.push(trunk_pgno);
all_pages.extend_from_slice(&trunk.leaf_pages);
current = trunk.next_trunk;
}
Ok(all_pages)
}
#[cfg(test)]
#[allow(clippy::similar_names)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_max_leaf_entries() {
assert_eq!(max_leaf_entries(4096), 1022);
assert_eq!(max_leaf_entries(512), 126);
}
#[test]
fn test_max_leaf_entries_saturates_on_tiny_pages() {
assert_eq!(max_leaf_entries(0), 0);
assert_eq!(max_leaf_entries(4), 0); assert_eq!(max_leaf_entries(8), 0); assert_eq!(max_leaf_entries(11), 0); assert_eq!(max_leaf_entries(12), 1); }
#[test]
fn test_trunk_parse_write_roundtrip() {
let trunk = FreelistTrunk {
next_trunk: Some(PageNumber::new(10).unwrap()),
leaf_pages: vec![
PageNumber::new(20).unwrap(),
PageNumber::new(30).unwrap(),
PageNumber::new(40).unwrap(),
],
};
let mut page = vec![0u8; 4096];
trunk.write(&mut page);
let parsed = FreelistTrunk::parse(&page).unwrap();
assert_eq!(parsed.next_trunk.unwrap().get(), 10);
assert_eq!(parsed.leaf_pages.len(), 3);
assert_eq!(parsed.leaf_pages[0].get(), 20);
assert_eq!(parsed.leaf_pages[1].get(), 30);
assert_eq!(parsed.leaf_pages[2].get(), 40);
}
#[test]
fn test_trunk_parse_last_in_chain() {
let trunk = FreelistTrunk {
next_trunk: None,
leaf_pages: vec![PageNumber::new(5).unwrap()],
};
let mut page = vec![0u8; 4096];
trunk.write(&mut page);
let parsed = FreelistTrunk::parse(&page).unwrap();
assert!(parsed.next_trunk.is_none());
assert_eq!(parsed.leaf_pages.len(), 1);
}
#[test]
fn test_trunk_parse_empty() {
let trunk = FreelistTrunk {
next_trunk: None,
leaf_pages: vec![],
};
let mut page = vec![0u8; 4096];
trunk.write(&mut page);
let parsed = FreelistTrunk::parse(&page).unwrap();
assert!(parsed.next_trunk.is_none());
assert!(parsed.leaf_pages.is_empty());
}
#[test]
fn test_trunk_parse_truncated() {
let page = vec![0u8; 4];
let result = FreelistTrunk::parse(&page);
assert!(result.is_err());
}
#[test]
fn test_trunk_parse_rejects_over_capacity_and_skips_zero_entries() {
let mut over = vec![0u8; 16];
over[4..8].copy_from_slice(&3u32.to_be_bytes()); assert!(
matches!(
FreelistTrunk::parse(&over),
Err(FrankenError::DatabaseCorrupt { .. })
),
"leaf_count over capacity must be rejected"
);
let mut page = vec![0u8; 4096];
page[0..4].copy_from_slice(&0u32.to_be_bytes()); page[4..8].copy_from_slice(&3u32.to_be_bytes()); page[8..12].copy_from_slice(&20u32.to_be_bytes());
page[12..16].copy_from_slice(&0u32.to_be_bytes()); page[16..20].copy_from_slice(&40u32.to_be_bytes());
let parsed = FreelistTrunk::parse(&page).unwrap();
assert!(parsed.next_trunk.is_none());
assert_eq!(parsed.leaf_pages.len(), 2, "zero entry must be skipped");
assert_eq!(parsed.leaf_pages[0].get(), 20);
assert_eq!(parsed.leaf_pages[1].get(), 40);
}
#[test]
fn test_freelist_allocate_from_free() {
let mut fl = Freelist::with_pages(
vec![PageNumber::new(10).unwrap(), PageNumber::new(20).unwrap()],
100,
4096,
);
assert_eq!(fl.free_count(), 2);
let p1 = fl.allocate().unwrap();
assert_eq!(p1.get(), 20); assert_eq!(fl.free_count(), 1);
let p2 = fl.allocate().unwrap();
assert_eq!(p2.get(), 10);
assert_eq!(fl.free_count(), 0);
}
#[test]
fn test_freelist_allocate_extends_db() {
let mut fl = Freelist::new(100, 4096);
assert_eq!(fl.free_count(), 0);
let p = fl.allocate().unwrap();
assert_eq!(p.get(), 101);
assert_eq!(fl.db_page_count(), 101);
let p2 = fl.allocate().unwrap();
assert_eq!(p2.get(), 102);
}
#[test]
fn test_freelist_deallocate() {
let mut fl = Freelist::new(100, 4096);
fl.deallocate(PageNumber::new(50).unwrap());
assert_eq!(fl.free_count(), 1);
let p = fl.allocate().unwrap();
assert_eq!(p.get(), 50);
}
#[test]
fn test_freelist_reuses_freed_page_before_extending_db() {
let mut fl = Freelist::new(100, 4096);
assert_eq!(fl.allocate().unwrap().get(), 101);
assert_eq!(fl.allocate().unwrap().get(), 102);
assert_eq!(fl.db_page_count(), 102);
fl.deallocate(PageNumber::new(101).unwrap());
assert_eq!(fl.free_count(), 1);
assert_eq!(
fl.allocate().unwrap().get(),
101,
"freed page must be reused before extending the db"
);
assert_eq!(fl.free_count(), 0);
assert_eq!(
fl.allocate().unwrap().get(),
103,
"extension must resume from the high-water mark, not the reused page"
);
assert_eq!(fl.db_page_count(), 103);
}
#[test]
fn test_freelist_max_page_count() {
let mut fl = Freelist::new(MAX_PAGE_COUNT, 4096);
assert!(fl.allocate().is_err());
}
#[test]
fn test_freelist_rejects_and_skips_pages_beyond_db_size() {
let mut fl = Freelist::new(10, 4096);
fl.deallocate(PageNumber::new(5).unwrap()); fl.deallocate(PageNumber::new(100).unwrap()); assert_eq!(
fl.free_count(),
1,
"out-of-range page must not enter the freelist"
);
assert_eq!(fl.allocate().unwrap().get(), 5);
let mut fl = Freelist::with_pages(
vec![PageNumber::new(99).unwrap(), PageNumber::new(5).unwrap()],
10,
4096,
);
assert_eq!(fl.allocate().unwrap().get(), 5);
assert_eq!(
fl.allocate().unwrap().get(),
11,
"stale entry skipped; db extended instead"
);
assert_eq!(fl.db_page_count(), 11);
}
#[test]
fn test_btree_freelist_reclamation() {
let mut freelist = Freelist::new(200, 4096);
let reclaimed = PageNumber::new(150).unwrap();
freelist.deallocate(reclaimed);
assert_eq!(freelist.free_count(), 1);
assert_eq!(freelist.allocate().unwrap(), reclaimed);
assert_eq!(freelist.free_count(), 0);
}
#[test]
fn test_read_freelist_single_trunk() {
let mut pages: HashMap<u32, Vec<u8>> = HashMap::new();
let trunk = FreelistTrunk {
next_trunk: None,
leaf_pages: vec![
PageNumber::new(5).unwrap(),
PageNumber::new(6).unwrap(),
PageNumber::new(7).unwrap(),
],
};
let mut page = vec![0u8; 4096];
trunk.write(&mut page);
pages.insert(3, page);
let result = read_freelist(Some(PageNumber::new(3).unwrap()), &mut |pgno| {
pages
.get(&pgno.get())
.cloned()
.ok_or_else(|| FrankenError::internal("page not found"))
})
.unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result[0].get(), 3); assert_eq!(result[1].get(), 5);
assert_eq!(result[2].get(), 6);
assert_eq!(result[3].get(), 7);
}
#[test]
fn test_read_freelist_multi_trunk() {
let mut pages: HashMap<u32, Vec<u8>> = HashMap::new();
let trunk2 = FreelistTrunk {
next_trunk: None,
leaf_pages: vec![PageNumber::new(9).unwrap()],
};
let mut page2 = vec![0u8; 4096];
trunk2.write(&mut page2);
pages.insert(8, page2);
let trunk1 = FreelistTrunk {
next_trunk: Some(PageNumber::new(8).unwrap()),
leaf_pages: vec![PageNumber::new(5).unwrap(), PageNumber::new(6).unwrap()],
};
let mut page1 = vec![0u8; 4096];
trunk1.write(&mut page1);
pages.insert(3, page1);
let result = read_freelist(Some(PageNumber::new(3).unwrap()), &mut |pgno| {
pages
.get(&pgno.get())
.cloned()
.ok_or_else(|| FrankenError::internal("page not found"))
})
.unwrap();
assert_eq!(result.len(), 5);
}
#[test]
fn test_read_freelist_none() {
let result = read_freelist(None, &mut |_pgno| {
Err(FrankenError::internal("should not be called"))
})
.unwrap();
assert!(result.is_empty());
}
#[test]
fn test_freelist_allocate_skips_pending_byte() {
let page_size = 4096;
let pending_byte_page = (0x4000_0000 / page_size) + 1;
let mut fl = Freelist::new(pending_byte_page - 1, page_size);
let p = fl.allocate().unwrap();
assert_eq!(p.get(), pending_byte_page + 1);
assert_eq!(fl.db_page_count(), pending_byte_page + 1);
}
#[test]
fn test_ptrmap_entries_per_page_4096() {
assert_eq!(ptrmap_entries_per_page(4096), 819);
assert_eq!(ptrmap_group_size(4096), 820);
}
#[test]
fn test_ptrmap_page_locations_4096() {
assert!(is_ptrmap_page(PageNumber::new(2).unwrap(), 4096, 4096));
assert!(is_ptrmap_page(PageNumber::new(822).unwrap(), 4096, 4096));
assert!(is_ptrmap_page(PageNumber::new(1642).unwrap(), 4096, 4096));
assert!(!is_ptrmap_page(PageNumber::new(3).unwrap(), 4096, 4096));
}
#[test]
fn test_ptrmap_page_for_given_pgno_boundaries() {
let p3 = PageNumber::new(3).unwrap();
let p821 = PageNumber::new(821).unwrap();
let p823 = PageNumber::new(823).unwrap();
assert_eq!(ptrmap_page_for(p3, 4096, 4096).unwrap().get(), 2);
assert_eq!(ptrmap_entry_offset(p3, 4096, 4096).unwrap(), 0);
assert_eq!(ptrmap_page_for(p821, 4096, 4096).unwrap().get(), 2);
assert_eq!(ptrmap_entry_offset(p821, 4096, 4096).unwrap(), 818 * 5);
assert_eq!(ptrmap_page_for(p823, 4096, 4096).unwrap().get(), 822);
assert_eq!(ptrmap_entry_offset(p823, 4096, 4096).unwrap(), 0);
assert!(ptrmap_page_for(PageNumber::new(822).unwrap(), 4096, 4096).is_none());
assert!(ptrmap_entry_offset(PageNumber::new(822).unwrap(), 4096, 4096).is_none());
}
#[test]
fn test_ptrmap_page_for_header_and_first_ptrmap_page_have_no_entry() {
let p1 = PageNumber::new(1).unwrap();
let p2 = PageNumber::new(2).unwrap();
assert!(ptrmap_page_for(p1, 4096, 4096).is_none());
assert!(ptrmap_entry_offset(p1, 4096, 4096).is_none());
assert!(is_ptrmap_page(p2, 4096, 4096));
assert!(ptrmap_page_for(p2, 4096, 4096).is_none());
assert!(ptrmap_entry_offset(p2, 4096, 4096).is_none());
}
#[test]
fn test_ptrmap_pending_byte_page_collision() {
let pending_byte_pgno = 1_048_577;
let usable_size = 1024;
let page_size = 1024;
assert!(!is_ptrmap_page(
PageNumber::new(pending_byte_pgno).unwrap(),
usable_size,
page_size
));
assert!(is_ptrmap_page(
PageNumber::new(pending_byte_pgno + 1).unwrap(),
usable_size,
page_size
));
assert!(
ptrmap_entry_offset(
PageNumber::new(pending_byte_pgno).unwrap(),
usable_size,
page_size
)
.is_none()
);
assert_eq!(
ptrmap_entry_offset(
PageNumber::new(pending_byte_pgno + 2).unwrap(),
usable_size,
page_size
),
Some(0)
);
}
#[test]
fn test_ptrmap_entry_offset_skips_pending_byte_inside_group() {
let usable_size = 4096;
let page_size = 4096;
let pending_byte_pgno = (PENDING_BYTE_OFFSET / page_size) + 1;
assert!(
ptrmap_entry_offset(
PageNumber::new(pending_byte_pgno).unwrap(),
usable_size,
page_size
)
.is_none()
);
let pgno = PageNumber::new(pending_byte_pgno + 1).unwrap();
assert_eq!(
ptrmap_page_for(pgno, usable_size, page_size).unwrap().get(),
261_582
);
assert_eq!(
ptrmap_entry_offset(pgno, usable_size, page_size),
Some(563 * PTRMAP_ENTRY_SIZE_BYTES)
);
}
#[test]
fn test_ptrmap_entry_offset_last_page_in_group_with_pending_byte() {
let usable_size = 4096;
let page_size = 4096;
let ptrmap_page = 261582;
let pgno = PageNumber::new(262402).unwrap();
assert_eq!(
ptrmap_page_for(pgno, usable_size, page_size),
None );
assert_eq!(ptrmap_entry_offset(pgno, usable_size, page_size), None);
let last_in_group = PageNumber::new(262401).unwrap();
assert_eq!(
ptrmap_page_for(last_in_group, usable_size, page_size)
.unwrap()
.get(),
ptrmap_page
);
let offset = ptrmap_entry_offset(last_in_group, usable_size, page_size).unwrap();
assert!(offset + PTRMAP_ENTRY_SIZE_BYTES <= usable_size);
assert_eq!(offset, 818 * PTRMAP_ENTRY_SIZE_BYTES);
}
#[test]
fn test_ptrmap_entry_encode_decode() {
let entry = PtrMapEntry {
kind: PtrMapType::Overflow1,
parent: Some(PageNumber::new(123).unwrap()),
};
let encoded = entry.encode();
let decoded = PtrMapEntry::decode(&encoded).unwrap();
assert_eq!(decoded, entry);
}
#[test]
fn test_ptrmap_type_parent_semantics() {
let root = PtrMapEntry {
kind: PtrMapType::RootPage,
parent: None,
};
let free = PtrMapEntry {
kind: PtrMapType::FreePage,
parent: None,
};
assert_eq!(PtrMapEntry::decode(&root.encode()).unwrap(), root);
assert_eq!(PtrMapEntry::decode(&free.encode()).unwrap(), free);
let invalid_root = [1, 0, 0, 0, 7];
assert!(PtrMapEntry::decode(&invalid_root).is_err());
let invalid_bt = [5, 0, 0, 0, 0];
assert!(PtrMapEntry::decode(&invalid_bt).is_err());
}
#[test]
fn test_ptrmap_entry_decode_rejects_short_input_and_invalid_type_code() {
assert!(PtrMapEntry::decode(&[]).is_err());
assert!(PtrMapEntry::decode(&[1, 0, 0, 0]).is_err());
assert!(PtrMapEntry::decode(&[0, 0, 0, 0, 0]).is_err());
assert!(PtrMapEntry::decode(&[6, 0, 0, 0, 1]).is_err());
assert!(PtrMapEntry::decode(&[1, 0, 0, 0, 0]).is_ok()); assert!(PtrMapEntry::decode(&[5, 0, 0, 0, 1]).is_ok()); }
#[test]
fn test_ptrmap_layout_self_consistency_across_a_page_range() {
let usable = 512u32;
let page_size = 512u32;
assert_eq!(ptrmap_entries_per_page(usable), 102);
let group = ptrmap_group_size(usable);
assert_eq!(group, 103);
for raw in 2u32..=250 {
let pg = PageNumber::new(raw).unwrap();
let is_map = is_ptrmap_page(pg, usable, page_size);
assert_eq!(
is_map,
(raw - 2) % group == 0,
"is_ptrmap_page wrong at {raw}"
);
match ptrmap_page_for(pg, usable, page_size) {
None => {
assert!(
is_map || raw < 3,
"page {raw} unexpectedly has no ptrmap page"
);
}
Some(q) => {
assert!(!is_map, "ptrmap page {raw} must not map to another");
assert!(
is_ptrmap_page(q, usable, page_size),
"page {raw} maps to non-ptrmap {q:?}"
);
assert!(q.get() < raw, "ptrmap page {q:?} must precede {raw}");
assert!(
raw - q.get() <= group,
"page {raw} lies outside its group {q:?}"
);
let off = ptrmap_entry_offset(pg, usable, page_size).expect("entry offset");
assert_eq!(
off,
(raw - q.get() - 1) * PTRMAP_ENTRY_SIZE_BYTES,
"entry offset wrong at {raw}"
);
assert!(off < usable, "entry offset {off} past usable {usable}");
}
}
}
}
}