Skip to main content

reddb_file/
file_format.rs

1//! Core persisted file-format constants.
2//!
3//! Runtime crates can own page management and columnar execution, but these
4//! durable magic/version values are file compatibility contracts.
5
6use std::fmt;
7
8/// Magic bytes for database file identification: `RDDB`.
9pub const PAGE_FILE_MAGIC: [u8; 4] = [0x52, 0x44, 0x44, 0x42];
10
11/// Database file version 1.0.0.
12pub const PAGE_FILE_VERSION: u32 = 0x0001_0000;
13
14/// Paged database page size.
15pub const PAGED_PAGE_SIZE: usize = 16_384;
16
17/// Paged database page-header size before the page-0 database header.
18pub const PAGED_PAGE_HEADER_SIZE: usize = 32;
19
20pub const PAGED_CELL_POINTER_SIZE: usize = 2;
21pub const PAGED_CELL_HEADER_SIZE: usize = 6;
22
23/// Raw persisted page header encoded at the start of every paged-store page.
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct PagedPageHeader {
26    pub page_type: u8,
27    pub flags: u8,
28    pub cell_count: u16,
29    pub free_start: u16,
30    pub free_end: u16,
31    pub page_id: u32,
32    pub parent_id: u32,
33    pub right_child: u32,
34    pub lsn: u64,
35    pub checksum: u32,
36}
37
38pub fn encode_paged_page_header(header: &PagedPageHeader) -> [u8; PAGED_PAGE_HEADER_SIZE] {
39    let mut buf = [0u8; PAGED_PAGE_HEADER_SIZE];
40    buf[0] = header.page_type;
41    buf[1] = header.flags;
42    buf[2..4].copy_from_slice(&header.cell_count.to_le_bytes());
43    buf[4..6].copy_from_slice(&header.free_start.to_le_bytes());
44    buf[6..8].copy_from_slice(&header.free_end.to_le_bytes());
45    buf[8..12].copy_from_slice(&header.page_id.to_le_bytes());
46    buf[12..16].copy_from_slice(&header.parent_id.to_le_bytes());
47    buf[16..20].copy_from_slice(&header.right_child.to_le_bytes());
48    buf[20..28].copy_from_slice(&header.lsn.to_le_bytes());
49    buf[28..32].copy_from_slice(&header.checksum.to_le_bytes());
50    buf
51}
52
53pub fn decode_paged_page_header(buf: &[u8; PAGED_PAGE_HEADER_SIZE]) -> PagedPageHeader {
54    PagedPageHeader {
55        page_type: buf[0],
56        flags: buf[1],
57        cell_count: u16::from_le_bytes([buf[2], buf[3]]),
58        free_start: u16::from_le_bytes([buf[4], buf[5]]),
59        free_end: u16::from_le_bytes([buf[6], buf[7]]),
60        page_id: u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
61        parent_id: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
62        right_child: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
63        lsn: u64::from_le_bytes([
64            buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
65        ]),
66        checksum: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]),
67    }
68}
69
70pub fn paged_page_type(page: &[u8; PAGED_PAGE_SIZE]) -> u8 {
71    page[0]
72}
73
74pub fn paged_page_id(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
75    read_u32(page, 8).expect("paged page has header")
76}
77
78pub fn paged_page_lsn(page: &[u8; PAGED_PAGE_SIZE]) -> u64 {
79    read_u64(page, 20).expect("paged page has header")
80}
81
82pub fn set_paged_page_lsn(page: &mut [u8; PAGED_PAGE_SIZE], lsn: u64) {
83    write_u64(page, 20, lsn).expect("paged page has header");
84}
85
86pub fn paged_page_cell_count(page: &[u8; PAGED_PAGE_SIZE]) -> u16 {
87    read_u16(page, 2).expect("paged page has header")
88}
89
90pub fn set_paged_page_cell_count(page: &mut [u8; PAGED_PAGE_SIZE], count: u16) {
91    write_u16(page, 2, count).expect("paged page has header");
92}
93
94pub fn paged_page_parent_id(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
95    read_u32(page, 12).expect("paged page has header")
96}
97
98pub fn set_paged_page_parent_id(page: &mut [u8; PAGED_PAGE_SIZE], parent_id: u32) {
99    write_u32(page, 12, parent_id).expect("paged page has header");
100}
101
102pub fn paged_page_right_child(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
103    read_u32(page, 16).expect("paged page has header")
104}
105
106pub fn set_paged_page_right_child(page: &mut [u8; PAGED_PAGE_SIZE], child_id: u32) {
107    write_u32(page, 16, child_id).expect("paged page has header");
108}
109
110pub fn paged_page_free_start(page: &[u8; PAGED_PAGE_SIZE]) -> u16 {
111    read_u16(page, 4).expect("paged page has header")
112}
113
114pub fn set_paged_page_free_start(page: &mut [u8; PAGED_PAGE_SIZE], offset: u16) {
115    write_u16(page, 4, offset).expect("paged page has header");
116}
117
118pub fn paged_page_free_end(page: &[u8; PAGED_PAGE_SIZE]) -> u16 {
119    read_u16(page, 6).expect("paged page has header")
120}
121
122pub fn set_paged_page_free_end(page: &mut [u8; PAGED_PAGE_SIZE], offset: u16) {
123    write_u16(page, 6, offset).expect("paged page has header");
124}
125
126pub fn paged_page_checksum(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
127    read_u32(page, 28).expect("paged page has header")
128}
129
130pub fn clear_paged_page_checksum(page: &mut [u8; PAGED_PAGE_SIZE]) {
131    write_u32(page, 28, 0).expect("paged page has header");
132}
133
134pub fn set_paged_page_checksum(page: &mut [u8; PAGED_PAGE_SIZE], checksum: u32) {
135    write_u32(page, 28, checksum).expect("paged page has header");
136}
137
138pub fn paged_cell_pointer_offset(index: usize) -> Option<usize> {
139    let offset = PAGED_PAGE_HEADER_SIZE.checked_add(index.checked_mul(PAGED_CELL_POINTER_SIZE)?)?;
140    (offset + PAGED_CELL_POINTER_SIZE <= PAGED_PAGE_SIZE).then_some(offset)
141}
142
143pub fn paged_cell_pointer(page: &[u8; PAGED_PAGE_SIZE], index: usize) -> Option<u16> {
144    read_u16(page, paged_cell_pointer_offset(index)?).ok()
145}
146
147pub fn set_paged_cell_pointer(
148    page: &mut [u8; PAGED_PAGE_SIZE],
149    index: usize,
150    pointer: u16,
151) -> bool {
152    let Some(offset) = paged_cell_pointer_offset(index) else {
153        return false;
154    };
155    if !paged_cell_pointer_is_valid(pointer) {
156        return false;
157    }
158    write_u16(page, offset, pointer).is_ok()
159}
160
161pub fn paged_cell_pointer_is_valid(pointer: u16) -> bool {
162    let pointer = pointer as usize;
163    (PAGED_PAGE_HEADER_SIZE..PAGED_PAGE_SIZE).contains(&pointer)
164}
165
166pub fn paged_cell_len(key_len: usize, value_len: usize) -> Option<usize> {
167    PAGED_CELL_HEADER_SIZE
168        .checked_add(key_len)?
169        .checked_add(value_len)
170}
171
172pub fn paged_cell_total_len(page: &[u8; PAGED_PAGE_SIZE], pointer: u16) -> Option<usize> {
173    let pointer = pointer as usize;
174    if pointer + PAGED_CELL_HEADER_SIZE > PAGED_PAGE_SIZE {
175        return None;
176    }
177    let key_len = read_u16(page, pointer).ok()? as usize;
178    let value_len = read_u32(page, pointer + 2).ok()? as usize;
179    let total_len = paged_cell_len(key_len, value_len)?;
180    (pointer + total_len <= PAGED_PAGE_SIZE).then_some(total_len)
181}
182
183pub fn paged_cell_bytes(page: &[u8; PAGED_PAGE_SIZE], pointer: u16) -> Option<&[u8]> {
184    let total_len = paged_cell_total_len(page, pointer)?;
185    let pointer = pointer as usize;
186    Some(&page[pointer..pointer + total_len])
187}
188
189pub fn paged_cell_key_value(cell: &[u8]) -> Option<(&[u8], &[u8])> {
190    if cell.len() < PAGED_CELL_HEADER_SIZE {
191        return None;
192    }
193    let key_len = u16::from_le_bytes(cell[0..2].try_into().ok()?) as usize;
194    let value_len = u32::from_le_bytes(cell[2..6].try_into().ok()?) as usize;
195    let total_len = paged_cell_len(key_len, value_len)?;
196    if total_len > cell.len() {
197        return None;
198    }
199    let key_start = PAGED_CELL_HEADER_SIZE;
200    let value_start = key_start + key_len;
201    Some((
202        &cell[key_start..value_start],
203        &cell[value_start..value_start + value_len],
204    ))
205}
206
207pub fn write_paged_cell(
208    page: &mut [u8; PAGED_PAGE_SIZE],
209    offset: u16,
210    key: &[u8],
211    value: &[u8],
212) -> bool {
213    let Ok(key_len) = u16::try_from(key.len()) else {
214        return false;
215    };
216    let Ok(value_len) = u32::try_from(value.len()) else {
217        return false;
218    };
219    let Some(total_len) = paged_cell_len(key.len(), value.len()) else {
220        return false;
221    };
222    let offset = offset as usize;
223    if offset + total_len > PAGED_PAGE_SIZE {
224        return false;
225    }
226
227    page[offset..offset + 2].copy_from_slice(&key_len.to_le_bytes());
228    page[offset + 2..offset + 6].copy_from_slice(&value_len.to_le_bytes());
229    page[offset + PAGED_CELL_HEADER_SIZE..offset + PAGED_CELL_HEADER_SIZE + key.len()]
230        .copy_from_slice(key);
231    page[offset + PAGED_CELL_HEADER_SIZE + key.len()..offset + total_len].copy_from_slice(value);
232    true
233}
234
235/// Encryption marker embedded in page 0 when page encryption is enabled.
236pub const PAGED_ENCRYPTION_MARKER: [u8; 4] = *b"RDBE";
237
238/// Current page-0 encryption marker offset.
239///
240/// This preserves the existing on-disk placement. It overlaps the historical
241/// physical-header region, so callers must preserve page-0 bytes around normal
242/// header writes.
243pub const PAGED_ENCRYPTION_MARKER_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 32;
244
245pub const PAGED_ENCRYPTION_SALT_SIZE: usize = 32;
246pub const PAGED_ENCRYPTION_KEY_CHECK_PLAINTEXT_SIZE: usize = 32;
247pub const PAGED_ENCRYPTION_KEY_CHECK_BLOB_SIZE: usize = 60;
248pub const PAGED_ENCRYPTION_HEADER_SIZE: usize =
249    PAGED_ENCRYPTION_SALT_SIZE + PAGED_ENCRYPTION_KEY_CHECK_BLOB_SIZE;
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct PagedEncryptionHeader {
253    pub salt: [u8; PAGED_ENCRYPTION_SALT_SIZE],
254    pub key_check: Vec<u8>,
255}
256
257pub fn encode_paged_encryption_header(header: &PagedEncryptionHeader) -> Vec<u8> {
258    let mut out = Vec::with_capacity(PAGED_ENCRYPTION_HEADER_SIZE);
259    out.extend_from_slice(&header.salt);
260    out.extend_from_slice(&header.key_check);
261    out
262}
263
264pub fn decode_paged_encryption_header(
265    data: &[u8],
266) -> Result<PagedEncryptionHeader, DatabaseHeaderError> {
267    ensure_len(data, PAGED_ENCRYPTION_HEADER_SIZE)?;
268    let mut salt = [0u8; PAGED_ENCRYPTION_SALT_SIZE];
269    salt.copy_from_slice(&data[..PAGED_ENCRYPTION_SALT_SIZE]);
270    let key_check = data[PAGED_ENCRYPTION_SALT_SIZE..PAGED_ENCRYPTION_HEADER_SIZE].to_vec();
271    Ok(PagedEncryptionHeader { salt, key_check })
272}
273
274pub fn paged_encryption_marker_present(page: &[u8]) -> bool {
275    page.get(PAGED_ENCRYPTION_MARKER_OFFSET..PAGED_ENCRYPTION_MARKER_OFFSET + 4)
276        == Some(&PAGED_ENCRYPTION_MARKER)
277}
278
279pub fn paged_encryption_header_bytes(page: &[u8]) -> Option<&[u8]> {
280    let start = PAGED_ENCRYPTION_MARKER_OFFSET + PAGED_ENCRYPTION_MARKER.len();
281    page.get(start..start + PAGED_ENCRYPTION_HEADER_SIZE)
282}
283
284pub fn write_paged_encryption_marker_and_header(
285    page: &mut [u8],
286    header_bytes: &[u8],
287) -> Result<(), DatabaseHeaderError> {
288    let marker_end = PAGED_ENCRYPTION_MARKER_OFFSET + PAGED_ENCRYPTION_MARKER.len();
289    let header_end = marker_end + header_bytes.len();
290    ensure_len(page, header_end)?;
291    page[PAGED_ENCRYPTION_MARKER_OFFSET..marker_end].copy_from_slice(&PAGED_ENCRYPTION_MARKER);
292    page[marker_end..header_end].copy_from_slice(header_bytes);
293    Ok(())
294}
295
296const DB_MAGIC_OFFSET: usize = PAGED_PAGE_HEADER_SIZE;
297const DB_VERSION_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 4;
298const DB_PAGE_SIZE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 8;
299const DB_PAGE_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 12;
300const DB_FREELIST_HEAD_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 16;
301const DB_SCHEMA_VERSION_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 20;
302const DB_CHECKPOINT_LSN_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 24;
303const DB_PHYSICAL_FORMAT_VERSION_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 32;
304const DB_PHYSICAL_SEQUENCE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 36;
305const DB_MANIFEST_ROOT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 44;
306const DB_MANIFEST_OLDEST_ROOT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 52;
307const DB_FREE_SET_ROOT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 60;
308const DB_MANIFEST_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 68;
309const DB_MANIFEST_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 72;
310const DB_COLLECTION_ROOTS_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 80;
311const DB_COLLECTION_ROOTS_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 84;
312const DB_COLLECTION_ROOT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 92;
313const DB_SNAPSHOT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 96;
314const DB_INDEX_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 100;
315const DB_CATALOG_COLLECTION_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 104;
316const DB_CATALOG_TOTAL_ENTITIES_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 108;
317const DB_EXPORT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 116;
318const DB_GRAPH_PROJECTION_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 120;
319const DB_ANALYTICS_JOB_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 124;
320const DB_MANIFEST_EVENT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 128;
321const DB_REGISTRY_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 132;
322const DB_REGISTRY_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 136;
323const DB_RECOVERY_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 144;
324const DB_RECOVERY_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 148;
325const DB_CATALOG_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 156;
326const DB_CATALOG_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 160;
327const DB_METADATA_STATE_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 168;
328const DB_METADATA_STATE_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 172;
329const DB_VECTOR_ARTIFACT_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 180;
330const DB_VECTOR_ARTIFACT_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 184;
331const DB_CHECKPOINT_IN_PROGRESS_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 192;
332const DB_CHECKPOINT_TARGET_LSN_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 193;
333const DB_HEADER_MIN_LEN: usize = DB_CHECKPOINT_TARGET_LSN_OFFSET + 8;
334
335/// Double-write-buffer file magic: `RDDW`.
336pub const DWB_MAGIC: [u8; 4] = [0x52, 0x44, 0x44, 0x57];
337pub const PAGED_DWB_HEADER_SIZE: usize = 12;
338pub const PAGED_DWB_ENTRY_HEADER_SIZE: usize = 4;
339pub const PAGED_DWB_ENTRY_SIZE: usize = PAGED_DWB_ENTRY_HEADER_SIZE + PAGED_PAGE_SIZE;
340
341#[derive(Debug, Clone, PartialEq, Eq)]
342pub struct PagedDwbEntry {
343    pub page_id: u32,
344    pub page: [u8; PAGED_PAGE_SIZE],
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub enum PagedDwbFrameError {
349    ShortHeader { got: usize },
350    InvalidMagic,
351    IncompleteFrame { expected: usize, got: usize },
352    ChecksumMismatch { expected: u32, actual: u32 },
353}
354
355impl fmt::Display for PagedDwbFrameError {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        match self {
358            Self::ShortHeader { got } => write!(f, "DWB frame too short: got {got} bytes"),
359            Self::InvalidMagic => write!(f, "invalid DWB frame magic"),
360            Self::IncompleteFrame { expected, got } => {
361                write!(
362                    f,
363                    "incomplete DWB frame: expected {expected} bytes, got {got}"
364                )
365            }
366            Self::ChecksumMismatch { expected, actual } => write!(
367                f,
368                "DWB frame checksum mismatch: expected {expected}, actual {actual}"
369            ),
370        }
371    }
372}
373
374impl std::error::Error for PagedDwbFrameError {}
375
376pub fn encode_paged_dwb_frame<'a>(
377    pages: impl IntoIterator<Item = (u32, &'a [u8; PAGED_PAGE_SIZE])>,
378) -> Vec<u8> {
379    let pages: Vec<_> = pages.into_iter().collect();
380    let total = PAGED_DWB_HEADER_SIZE + pages.len() * PAGED_DWB_ENTRY_SIZE;
381    let mut out = Vec::with_capacity(total);
382
383    out.extend_from_slice(&DWB_MAGIC);
384    out.extend_from_slice(&(pages.len() as u32).to_le_bytes());
385    out.extend_from_slice(&[0u8; 4]);
386
387    for (page_id, page) in pages {
388        out.extend_from_slice(&page_id.to_le_bytes());
389        out.extend_from_slice(page);
390    }
391
392    let checksum = crc32(&out[PAGED_DWB_HEADER_SIZE..]);
393    out[8..12].copy_from_slice(&checksum.to_le_bytes());
394    out
395}
396
397pub fn decode_paged_dwb_frame(bytes: &[u8]) -> Result<Vec<PagedDwbEntry>, PagedDwbFrameError> {
398    if bytes.len() < PAGED_DWB_HEADER_SIZE {
399        return Err(PagedDwbFrameError::ShortHeader { got: bytes.len() });
400    }
401    if bytes[0..4] != DWB_MAGIC {
402        return Err(PagedDwbFrameError::InvalidMagic);
403    }
404
405    let count = u32::from_le_bytes(bytes[4..8].try_into().expect("len checked")) as usize;
406    let stored_checksum = u32::from_le_bytes(bytes[8..12].try_into().expect("len checked"));
407    let expected_len = PAGED_DWB_HEADER_SIZE + count * PAGED_DWB_ENTRY_SIZE;
408    if bytes.len() < expected_len {
409        return Err(PagedDwbFrameError::IncompleteFrame {
410            expected: expected_len,
411            got: bytes.len(),
412        });
413    }
414
415    let actual_checksum = crc32(&bytes[PAGED_DWB_HEADER_SIZE..expected_len]);
416    if actual_checksum != stored_checksum {
417        return Err(PagedDwbFrameError::ChecksumMismatch {
418            expected: stored_checksum,
419            actual: actual_checksum,
420        });
421    }
422
423    let mut offset = PAGED_DWB_HEADER_SIZE;
424    let mut entries = Vec::with_capacity(count);
425    for _ in 0..count {
426        let page_id =
427            u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("len checked"));
428        offset += PAGED_DWB_ENTRY_HEADER_SIZE;
429        let mut page = [0u8; PAGED_PAGE_SIZE];
430        page.copy_from_slice(&bytes[offset..offset + PAGED_PAGE_SIZE]);
431        offset += PAGED_PAGE_SIZE;
432        entries.push(PagedDwbEntry { page_id, page });
433    }
434    Ok(entries)
435}
436
437fn crc32(data: &[u8]) -> u32 {
438    crc32fast::hash(data)
439}
440
441/// `b"RDCC"` — RedDB Columnar Chunk. Opens and closes every column block.
442pub const COLUMN_BLOCK_MAGIC: [u8; 4] = *b"RDCC";
443
444/// Column block on-disk format version.
445pub const COLUMN_BLOCK_VERSION_V1: u16 = 1;
446
447/// Reusable segment-level bloom frame v2 magic.
448pub const BLOOM_SEGMENT_V2_MAGIC: u8 = 0xC0;
449
450/// Legacy vector B-tree on-disk page format.
451pub const VECTOR_BTREE_FORMAT_VERSION_V1: u16 = 1;
452
453/// Current vector B-tree on-disk page format.
454pub const VECTOR_BTREE_FORMAT_VERSION_V2: u16 = 2;
455
456/// Vector B-tree format stamped into freshly-written page headers.
457pub const VECTOR_BTREE_FORMAT_VERSION: u16 = VECTOR_BTREE_FORMAT_VERSION_V2;
458
459/// Database file header information persisted in page 0 after the page header.
460#[derive(Debug, Clone, PartialEq, Eq)]
461pub struct DatabaseHeader {
462    pub version: u32,
463    pub page_size: u32,
464    pub page_count: u32,
465    pub freelist_head: u32,
466    pub schema_version: u32,
467    pub checkpoint_lsn: u64,
468    pub checkpoint_in_progress: bool,
469    pub checkpoint_target_lsn: u64,
470    pub physical: PhysicalFileHeader,
471}
472
473impl Default for DatabaseHeader {
474    fn default() -> Self {
475        Self {
476            version: PAGE_FILE_VERSION,
477            page_size: PAGED_PAGE_SIZE as u32,
478            page_count: 1,
479            freelist_head: 0,
480            schema_version: 0,
481            checkpoint_lsn: 0,
482            checkpoint_in_progress: false,
483            checkpoint_target_lsn: 0,
484            physical: PhysicalFileHeader::default(),
485        }
486    }
487}
488
489/// Minimal physical state mirrored into page 0 for paged databases.
490///
491/// The pager owns when this is read and written, but the field layout is a
492/// durable file compatibility contract.
493#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
494pub struct PhysicalFileHeader {
495    pub format_version: u32,
496    pub sequence: u64,
497    pub manifest_oldest_root: u64,
498    pub manifest_root: u64,
499    pub free_set_root: u64,
500    pub manifest_page: u32,
501    pub manifest_checksum: u64,
502    pub collection_roots_page: u32,
503    pub collection_roots_checksum: u64,
504    pub collection_root_count: u32,
505    pub snapshot_count: u32,
506    pub index_count: u32,
507    pub catalog_collection_count: u32,
508    pub catalog_total_entities: u64,
509    pub export_count: u32,
510    pub graph_projection_count: u32,
511    pub analytics_job_count: u32,
512    pub manifest_event_count: u32,
513    pub registry_page: u32,
514    pub registry_checksum: u64,
515    pub recovery_page: u32,
516    pub recovery_checksum: u64,
517    pub catalog_page: u32,
518    pub catalog_checksum: u64,
519    pub metadata_state_page: u32,
520    pub metadata_state_checksum: u64,
521    pub vector_artifact_page: u32,
522    pub vector_artifact_checksum: u64,
523}
524
525#[derive(Debug, Clone, PartialEq, Eq)]
526pub enum DatabaseHeaderError {
527    ShortPage { need: usize, got: usize },
528    InvalidMagic,
529    UnsupportedPageSize(u32),
530    UnsupportedDatabaseVersion { file_version: u32, supported: u32 },
531}
532
533impl fmt::Display for DatabaseHeaderError {
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        match self {
536            Self::ShortPage { need, got } => {
537                write!(f, "database header page too short: need {need} bytes, got {got}")
538            }
539            Self::InvalidMagic => write!(f, "invalid database header magic"),
540            Self::UnsupportedPageSize(size) => write!(f, "Unsupported page size: {size}"),
541            Self::UnsupportedDatabaseVersion {
542                file_version,
543                supported,
544            } => write!(
545                f,
546                "Unsupported database version: file version {file_version} is newer than supported {supported}"
547            ),
548        }
549    }
550}
551
552impl std::error::Error for DatabaseHeaderError {}
553
554pub fn database_header_magic_matches(page: &[u8]) -> bool {
555    page.get(DB_MAGIC_OFFSET..DB_MAGIC_OFFSET + PAGE_FILE_MAGIC.len()) == Some(&PAGE_FILE_MAGIC)
556}
557
558pub fn init_database_header_page(
559    page: &mut [u8],
560    page_count: u32,
561) -> Result<(), DatabaseHeaderError> {
562    encode_database_header(
563        page,
564        &DatabaseHeader {
565            page_count,
566            ..DatabaseHeader::default()
567        },
568    )
569}
570
571pub fn database_header_page_count(page: &[u8]) -> Result<u32, DatabaseHeaderError> {
572    read_u32(page, DB_PAGE_COUNT_OFFSET)
573}
574
575pub fn set_database_header_version(
576    page: &mut [u8],
577    version: u32,
578) -> Result<(), DatabaseHeaderError> {
579    write_u32(page, DB_VERSION_OFFSET, version)
580}
581
582pub fn set_database_header_page_count(
583    page: &mut [u8],
584    page_count: u32,
585) -> Result<(), DatabaseHeaderError> {
586    write_u32(page, DB_PAGE_COUNT_OFFSET, page_count)
587}
588
589pub fn database_header_freelist_head(page: &[u8]) -> Result<u32, DatabaseHeaderError> {
590    read_u32(page, DB_FREELIST_HEAD_OFFSET)
591}
592
593pub fn set_database_header_freelist_head(
594    page: &mut [u8],
595    page_id: u32,
596) -> Result<(), DatabaseHeaderError> {
597    write_u32(page, DB_FREELIST_HEAD_OFFSET, page_id)
598}
599
600pub fn database_header_page_size(page: &[u8]) -> Result<u32, DatabaseHeaderError> {
601    read_u32(page, DB_PAGE_SIZE_OFFSET)
602}
603
604pub fn decode_database_header(page: &[u8]) -> Result<DatabaseHeader, DatabaseHeaderError> {
605    ensure_len(page, DB_HEADER_MIN_LEN)?;
606    if !database_header_magic_matches(page) {
607        return Err(DatabaseHeaderError::InvalidMagic);
608    }
609
610    let version = read_u32(page, DB_VERSION_OFFSET)?;
611    let page_size = read_u32(page, DB_PAGE_SIZE_OFFSET)?;
612    if page_size != PAGED_PAGE_SIZE as u32 {
613        return Err(DatabaseHeaderError::UnsupportedPageSize(page_size));
614    }
615    if version > PAGE_FILE_VERSION {
616        return Err(DatabaseHeaderError::UnsupportedDatabaseVersion {
617            file_version: version,
618            supported: PAGE_FILE_VERSION,
619        });
620    }
621
622    Ok(DatabaseHeader {
623        version,
624        page_size,
625        page_count: read_u32(page, DB_PAGE_COUNT_OFFSET)?,
626        freelist_head: read_u32(page, DB_FREELIST_HEAD_OFFSET)?,
627        schema_version: read_u32(page, DB_SCHEMA_VERSION_OFFSET)?,
628        checkpoint_lsn: read_u64(page, DB_CHECKPOINT_LSN_OFFSET)?,
629        checkpoint_in_progress: page[DB_CHECKPOINT_IN_PROGRESS_OFFSET] != 0,
630        checkpoint_target_lsn: read_u64(page, DB_CHECKPOINT_TARGET_LSN_OFFSET)?,
631        physical: PhysicalFileHeader {
632            format_version: read_u32(page, DB_PHYSICAL_FORMAT_VERSION_OFFSET)?,
633            sequence: read_u64(page, DB_PHYSICAL_SEQUENCE_OFFSET)?,
634            manifest_oldest_root: read_u64(page, DB_MANIFEST_OLDEST_ROOT_OFFSET)?,
635            manifest_root: read_u64(page, DB_MANIFEST_ROOT_OFFSET)?,
636            free_set_root: read_u64(page, DB_FREE_SET_ROOT_OFFSET)?,
637            manifest_page: read_u32(page, DB_MANIFEST_PAGE_OFFSET)?,
638            manifest_checksum: read_u64(page, DB_MANIFEST_CHECKSUM_OFFSET)?,
639            collection_roots_page: read_u32(page, DB_COLLECTION_ROOTS_PAGE_OFFSET)?,
640            collection_roots_checksum: read_u64(page, DB_COLLECTION_ROOTS_CHECKSUM_OFFSET)?,
641            collection_root_count: read_u32(page, DB_COLLECTION_ROOT_COUNT_OFFSET)?,
642            snapshot_count: read_u32(page, DB_SNAPSHOT_COUNT_OFFSET)?,
643            index_count: read_u32(page, DB_INDEX_COUNT_OFFSET)?,
644            catalog_collection_count: read_u32(page, DB_CATALOG_COLLECTION_COUNT_OFFSET)?,
645            catalog_total_entities: read_u64(page, DB_CATALOG_TOTAL_ENTITIES_OFFSET)?,
646            export_count: read_u32(page, DB_EXPORT_COUNT_OFFSET)?,
647            graph_projection_count: read_u32(page, DB_GRAPH_PROJECTION_COUNT_OFFSET)?,
648            analytics_job_count: read_u32(page, DB_ANALYTICS_JOB_COUNT_OFFSET)?,
649            manifest_event_count: read_u32(page, DB_MANIFEST_EVENT_COUNT_OFFSET)?,
650            registry_page: read_u32(page, DB_REGISTRY_PAGE_OFFSET)?,
651            registry_checksum: read_u64(page, DB_REGISTRY_CHECKSUM_OFFSET)?,
652            recovery_page: read_u32(page, DB_RECOVERY_PAGE_OFFSET)?,
653            recovery_checksum: read_u64(page, DB_RECOVERY_CHECKSUM_OFFSET)?,
654            catalog_page: read_u32(page, DB_CATALOG_PAGE_OFFSET)?,
655            catalog_checksum: read_u64(page, DB_CATALOG_CHECKSUM_OFFSET)?,
656            metadata_state_page: read_u32(page, DB_METADATA_STATE_PAGE_OFFSET)?,
657            metadata_state_checksum: read_u64(page, DB_METADATA_STATE_CHECKSUM_OFFSET)?,
658            vector_artifact_page: read_u32(page, DB_VECTOR_ARTIFACT_PAGE_OFFSET)?,
659            vector_artifact_checksum: read_u64(page, DB_VECTOR_ARTIFACT_CHECKSUM_OFFSET)?,
660        },
661    })
662}
663
664pub fn encode_database_header(
665    page: &mut [u8],
666    header: &DatabaseHeader,
667) -> Result<(), DatabaseHeaderError> {
668    ensure_len(page, DB_HEADER_MIN_LEN)?;
669    page[DB_MAGIC_OFFSET..DB_MAGIC_OFFSET + PAGE_FILE_MAGIC.len()]
670        .copy_from_slice(&PAGE_FILE_MAGIC);
671    write_u32(page, DB_VERSION_OFFSET, header.version)?;
672    write_u32(page, DB_PAGE_SIZE_OFFSET, header.page_size)?;
673    write_u32(page, DB_PAGE_COUNT_OFFSET, header.page_count)?;
674    write_u32(page, DB_FREELIST_HEAD_OFFSET, header.freelist_head)?;
675    write_u32(page, DB_SCHEMA_VERSION_OFFSET, header.schema_version)?;
676    write_u64(page, DB_CHECKPOINT_LSN_OFFSET, header.checkpoint_lsn)?;
677    write_u32(
678        page,
679        DB_PHYSICAL_FORMAT_VERSION_OFFSET,
680        header.physical.format_version,
681    )?;
682    write_u64(page, DB_PHYSICAL_SEQUENCE_OFFSET, header.physical.sequence)?;
683    write_u64(page, DB_MANIFEST_ROOT_OFFSET, header.physical.manifest_root)?;
684    write_u64(
685        page,
686        DB_MANIFEST_OLDEST_ROOT_OFFSET,
687        header.physical.manifest_oldest_root,
688    )?;
689    write_u64(page, DB_FREE_SET_ROOT_OFFSET, header.physical.free_set_root)?;
690    write_u32(page, DB_MANIFEST_PAGE_OFFSET, header.physical.manifest_page)?;
691    write_u64(
692        page,
693        DB_MANIFEST_CHECKSUM_OFFSET,
694        header.physical.manifest_checksum,
695    )?;
696    write_u32(
697        page,
698        DB_COLLECTION_ROOTS_PAGE_OFFSET,
699        header.physical.collection_roots_page,
700    )?;
701    write_u64(
702        page,
703        DB_COLLECTION_ROOTS_CHECKSUM_OFFSET,
704        header.physical.collection_roots_checksum,
705    )?;
706    write_u32(
707        page,
708        DB_COLLECTION_ROOT_COUNT_OFFSET,
709        header.physical.collection_root_count,
710    )?;
711    write_u32(
712        page,
713        DB_SNAPSHOT_COUNT_OFFSET,
714        header.physical.snapshot_count,
715    )?;
716    write_u32(page, DB_INDEX_COUNT_OFFSET, header.physical.index_count)?;
717    write_u32(
718        page,
719        DB_CATALOG_COLLECTION_COUNT_OFFSET,
720        header.physical.catalog_collection_count,
721    )?;
722    write_u64(
723        page,
724        DB_CATALOG_TOTAL_ENTITIES_OFFSET,
725        header.physical.catalog_total_entities,
726    )?;
727    write_u32(page, DB_EXPORT_COUNT_OFFSET, header.physical.export_count)?;
728    write_u32(
729        page,
730        DB_GRAPH_PROJECTION_COUNT_OFFSET,
731        header.physical.graph_projection_count,
732    )?;
733    write_u32(
734        page,
735        DB_ANALYTICS_JOB_COUNT_OFFSET,
736        header.physical.analytics_job_count,
737    )?;
738    write_u32(
739        page,
740        DB_MANIFEST_EVENT_COUNT_OFFSET,
741        header.physical.manifest_event_count,
742    )?;
743    write_u32(page, DB_REGISTRY_PAGE_OFFSET, header.physical.registry_page)?;
744    write_u64(
745        page,
746        DB_REGISTRY_CHECKSUM_OFFSET,
747        header.physical.registry_checksum,
748    )?;
749    write_u32(page, DB_RECOVERY_PAGE_OFFSET, header.physical.recovery_page)?;
750    write_u64(
751        page,
752        DB_RECOVERY_CHECKSUM_OFFSET,
753        header.physical.recovery_checksum,
754    )?;
755    write_u32(page, DB_CATALOG_PAGE_OFFSET, header.physical.catalog_page)?;
756    write_u64(
757        page,
758        DB_CATALOG_CHECKSUM_OFFSET,
759        header.physical.catalog_checksum,
760    )?;
761    write_u32(
762        page,
763        DB_METADATA_STATE_PAGE_OFFSET,
764        header.physical.metadata_state_page,
765    )?;
766    write_u64(
767        page,
768        DB_METADATA_STATE_CHECKSUM_OFFSET,
769        header.physical.metadata_state_checksum,
770    )?;
771    write_u32(
772        page,
773        DB_VECTOR_ARTIFACT_PAGE_OFFSET,
774        header.physical.vector_artifact_page,
775    )?;
776    write_u64(
777        page,
778        DB_VECTOR_ARTIFACT_CHECKSUM_OFFSET,
779        header.physical.vector_artifact_checksum,
780    )?;
781    page[DB_CHECKPOINT_IN_PROGRESS_OFFSET] = if header.checkpoint_in_progress { 1 } else { 0 };
782    write_u64(
783        page,
784        DB_CHECKPOINT_TARGET_LSN_OFFSET,
785        header.checkpoint_target_lsn,
786    )
787}
788
789// ── Paged superblock zone (ADR 0038 §2 phase 1) ─────────────────────────
790//
791// The first two sectors of a paged `.rdb` are the superblock zone: a
792// ping-pong pair of fixed-size slots. Each slot holds the page-0 image (page
793// header + database header + encryption header) followed by a trailer that
794// carries the slot's copy index, generation counter and CRC.
795//
796// An update always writes the *older* slot and fsyncs before it becomes the
797// newest valid copy, so a crash mid-write can only ever damage the copy that
798// was already stale. `select_paged_superblock` picks the highest generation
799// whose CRC verifies; both invalid means the store does not open.
800//
801// The zone lives inside page 0's 16 KiB, whose tail was unused. Page 0 is
802// therefore no longer a checksummed `Page` — the slot CRCs are its integrity
803// contract.
804
805/// Bytes per superblock slot. Sector-aligned so one slot write never touches
806/// its sibling.
807pub const PAGED_SUPERBLOCK_SLOT_SIZE: usize = 4096;
808
809/// The ping-pong pair.
810pub const PAGED_SUPERBLOCK_SLOT_COUNT: usize = 2;
811
812/// Total bytes reserved for the superblock zone at the head of the file.
813pub const PAGED_SUPERBLOCK_ZONE_SIZE: usize =
814    PAGED_SUPERBLOCK_SLOT_SIZE * PAGED_SUPERBLOCK_SLOT_COUNT;
815
816const PAGED_SUPERBLOCK_MAGIC: [u8; 8] = *b"RDBPSBK1";
817const PAGED_SUPERBLOCK_SLOT_VERSION: u32 = 1;
818
819/// Offset of the trailer within a slot: magic(8) ‖ version(4) ‖ copy(4) ‖
820/// generation(8) ‖ crc(4) ‖ reserved(4). Everything before it is the page-0
821/// image, which is why the offline migration tool can strip the trailer to
822/// recover the sidecar-era page 0 byte for byte.
823pub const PAGED_SUPERBLOCK_TRAILER_OFFSET: usize = PAGED_SUPERBLOCK_SLOT_SIZE - 32;
824const PAGED_SUPERBLOCK_VERSION_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 8;
825const PAGED_SUPERBLOCK_COPY_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 12;
826const PAGED_SUPERBLOCK_GENERATION_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 16;
827const PAGED_SUPERBLOCK_CRC_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 24;
828
829/// The newest valid copy found in a superblock zone.
830#[derive(Debug, Clone, Copy, PartialEq, Eq)]
831pub struct PagedSuperblockSelection {
832    pub copy_index: usize,
833    pub generation: u64,
834}
835
836/// Byte offset of `copy_index`'s slot from the start of the file.
837pub fn paged_superblock_slot_offset(copy_index: usize) -> u64 {
838    (copy_index % PAGED_SUPERBLOCK_SLOT_COUNT) as u64 * PAGED_SUPERBLOCK_SLOT_SIZE as u64
839}
840
841/// The copy a fresh update must target: always the one that is *not* newest,
842/// so the newest durable copy survives a crash during the write.
843pub fn paged_superblock_next_copy(newest: Option<PagedSuperblockSelection>) -> usize {
844    match newest {
845        Some(selection) => (selection.copy_index + 1) % PAGED_SUPERBLOCK_SLOT_COUNT,
846        None => 0,
847    }
848}
849
850/// Stamp `slot`'s trailer (copy index, generation) and seal it with a CRC over
851/// everything that precedes the checksum field.
852pub fn seal_paged_superblock_slot(
853    slot: &mut [u8],
854    copy_index: usize,
855    generation: u64,
856) -> Result<(), DatabaseHeaderError> {
857    ensure_len(slot, PAGED_SUPERBLOCK_SLOT_SIZE)?;
858    slot[PAGED_SUPERBLOCK_TRAILER_OFFSET..PAGED_SUPERBLOCK_TRAILER_OFFSET + 8]
859        .copy_from_slice(&PAGED_SUPERBLOCK_MAGIC);
860    write_u32(
861        slot,
862        PAGED_SUPERBLOCK_VERSION_OFFSET,
863        PAGED_SUPERBLOCK_SLOT_VERSION,
864    )?;
865    write_u32(
866        slot,
867        PAGED_SUPERBLOCK_COPY_OFFSET,
868        u32::try_from(copy_index).unwrap_or(u32::MAX),
869    )?;
870    write_u64(slot, PAGED_SUPERBLOCK_GENERATION_OFFSET, generation)?;
871    let crc = paged_superblock_crc(&slot[..PAGED_SUPERBLOCK_CRC_OFFSET]);
872    write_u32(slot, PAGED_SUPERBLOCK_CRC_OFFSET, crc)?;
873    // The 4 trailing reserved bytes stay zero.
874    write_u32(slot, PAGED_SUPERBLOCK_CRC_OFFSET + 4, 0)
875}
876
877/// Decode `slot`'s generation, or `None` when the slot was never written, is
878/// torn, carries a foreign copy index, or fails its CRC.
879pub fn paged_superblock_slot_generation(slot: &[u8], copy_index: usize) -> Option<u64> {
880    if slot.len() < PAGED_SUPERBLOCK_SLOT_SIZE {
881        return None;
882    }
883    if slot[PAGED_SUPERBLOCK_TRAILER_OFFSET..PAGED_SUPERBLOCK_TRAILER_OFFSET + 8]
884        != PAGED_SUPERBLOCK_MAGIC
885    {
886        return None;
887    }
888    if read_u32(slot, PAGED_SUPERBLOCK_VERSION_OFFSET).ok()? != PAGED_SUPERBLOCK_SLOT_VERSION {
889        return None;
890    }
891    // A slot that names a different copy is a misdirected write, not a
892    // legitimately older generation.
893    if read_u32(slot, PAGED_SUPERBLOCK_COPY_OFFSET).ok()? != u32::try_from(copy_index).ok()? {
894        return None;
895    }
896    let stored = read_u32(slot, PAGED_SUPERBLOCK_CRC_OFFSET).ok()?;
897    if paged_superblock_crc(&slot[..PAGED_SUPERBLOCK_CRC_OFFSET]) != stored {
898        return None;
899    }
900    read_u64(slot, PAGED_SUPERBLOCK_GENERATION_OFFSET).ok()
901}
902
903/// Pick the newest valid copy in a full superblock zone. `None` means both
904/// copies are invalid: the caller must refuse to open the store.
905pub fn select_paged_superblock(zone: &[u8]) -> Option<PagedSuperblockSelection> {
906    (0..PAGED_SUPERBLOCK_SLOT_COUNT)
907        .filter_map(|copy_index| {
908            let start = copy_index * PAGED_SUPERBLOCK_SLOT_SIZE;
909            let slot = zone.get(start..start + PAGED_SUPERBLOCK_SLOT_SIZE)?;
910            Some(PagedSuperblockSelection {
911                copy_index,
912                generation: paged_superblock_slot_generation(slot, copy_index)?,
913            })
914        })
915        .max_by_key(|selection| selection.generation)
916}
917
918fn paged_superblock_crc(bytes: &[u8]) -> u32 {
919    let mut hasher = crc32fast::Hasher::new();
920    hasher.update(bytes);
921    hasher.finalize()
922}
923
924fn ensure_len(bytes: &[u8], need: usize) -> Result<(), DatabaseHeaderError> {
925    if bytes.len() < need {
926        return Err(DatabaseHeaderError::ShortPage {
927            need,
928            got: bytes.len(),
929        });
930    }
931    Ok(())
932}
933
934fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, DatabaseHeaderError> {
935    ensure_len(bytes, offset + 4)?;
936    Ok(u32::from_le_bytes(
937        bytes[offset..offset + 4].try_into().expect("len checked"),
938    ))
939}
940
941fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, DatabaseHeaderError> {
942    ensure_len(bytes, offset + 2)?;
943    Ok(u16::from_le_bytes(
944        bytes[offset..offset + 2].try_into().expect("len checked"),
945    ))
946}
947
948fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, DatabaseHeaderError> {
949    ensure_len(bytes, offset + 8)?;
950    Ok(u64::from_le_bytes(
951        bytes[offset..offset + 8].try_into().expect("len checked"),
952    ))
953}
954
955fn write_u32(bytes: &mut [u8], offset: usize, value: u32) -> Result<(), DatabaseHeaderError> {
956    ensure_len(bytes, offset + 4)?;
957    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
958    Ok(())
959}
960
961fn write_u16(bytes: &mut [u8], offset: usize, value: u16) -> Result<(), DatabaseHeaderError> {
962    ensure_len(bytes, offset + 2)?;
963    bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
964    Ok(())
965}
966
967fn write_u64(bytes: &mut [u8], offset: usize, value: u64) -> Result<(), DatabaseHeaderError> {
968    ensure_len(bytes, offset + 8)?;
969    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
970    Ok(())
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976
977    fn sealed_zone(gen_a: Option<u64>, gen_b: Option<u64>) -> Vec<u8> {
978        let mut zone = vec![0u8; PAGED_SUPERBLOCK_ZONE_SIZE];
979        for (copy_index, generation) in [gen_a, gen_b].into_iter().enumerate() {
980            let Some(generation) = generation else {
981                continue;
982            };
983            let start = copy_index * PAGED_SUPERBLOCK_SLOT_SIZE;
984            let slot = &mut zone[start..start + PAGED_SUPERBLOCK_SLOT_SIZE];
985            init_database_header_page(slot, 3).expect("init header");
986            seal_paged_superblock_slot(slot, copy_index, generation).expect("seal slot");
987        }
988        zone
989    }
990
991    #[test]
992    fn superblock_zone_fits_two_sector_aligned_slots_before_page_one() {
993        assert_eq!(PAGED_SUPERBLOCK_SLOT_SIZE, 4096);
994        assert_eq!(PAGED_SUPERBLOCK_ZONE_SIZE, 8192);
995        // Both slots live inside page 0, so page 1 is never touched by a
996        // superblock update.
997        assert!(PAGED_SUPERBLOCK_ZONE_SIZE <= PAGED_PAGE_SIZE);
998        // The database header must not overrun into the slot trailer.
999        assert!(DB_HEADER_MIN_LEN < PAGED_SUPERBLOCK_TRAILER_OFFSET);
1000        assert_eq!(paged_superblock_slot_offset(0), 0);
1001        assert_eq!(paged_superblock_slot_offset(1), 4096);
1002    }
1003
1004    #[test]
1005    fn sealed_slot_round_trips_generation_and_keeps_the_header_readable() {
1006        let mut slot = vec![0u8; PAGED_SUPERBLOCK_SLOT_SIZE];
1007        init_database_header_page(&mut slot, 7).expect("init header");
1008        seal_paged_superblock_slot(&mut slot, 1, 42).expect("seal slot");
1009
1010        assert_eq!(paged_superblock_slot_generation(&slot, 1), Some(42));
1011        assert_eq!(
1012            database_header_page_count(&slot).expect("database_header_page_count() should succeed"),
1013            7
1014        );
1015    }
1016
1017    #[test]
1018    fn selection_prefers_the_newest_valid_copy_and_survives_one_torn_slot() {
1019        let zone = sealed_zone(Some(4), Some(5));
1020        assert_eq!(
1021            select_paged_superblock(&zone),
1022            Some(PagedSuperblockSelection {
1023                copy_index: 1,
1024                generation: 5
1025            })
1026        );
1027
1028        // Tear the newer copy: open falls back to the older intact one.
1029        let mut torn = zone.clone();
1030        torn[PAGED_SUPERBLOCK_SLOT_SIZE + 64] ^= 0xFF;
1031        assert_eq!(
1032            select_paged_superblock(&torn),
1033            Some(PagedSuperblockSelection {
1034                copy_index: 0,
1035                generation: 4
1036            })
1037        );
1038    }
1039
1040    #[test]
1041    fn a_misdirected_slot_copy_is_rejected_not_read_as_an_older_generation() {
1042        // Slot 0's bytes written at slot 1's offset: right data, wrong place.
1043        let zone = sealed_zone(Some(9), None);
1044        let (slot_zero, _) = zone.split_at(PAGED_SUPERBLOCK_SLOT_SIZE);
1045        assert_eq!(paged_superblock_slot_generation(slot_zero, 1), None);
1046    }
1047
1048    #[test]
1049    fn both_copies_invalid_yields_no_selection() {
1050        assert_eq!(
1051            select_paged_superblock(&vec![0u8; PAGED_SUPERBLOCK_ZONE_SIZE]),
1052            None
1053        );
1054        assert_eq!(select_paged_superblock(&[]), None);
1055
1056        let mut zone = sealed_zone(Some(1), Some(2));
1057        zone[64] ^= 0xFF;
1058        zone[PAGED_SUPERBLOCK_SLOT_SIZE + 64] ^= 0xFF;
1059        assert_eq!(select_paged_superblock(&zone), None);
1060    }
1061
1062    #[test]
1063    fn the_next_copy_to_write_is_always_the_stale_one() {
1064        assert_eq!(paged_superblock_next_copy(None), 0);
1065        assert_eq!(
1066            paged_superblock_next_copy(Some(PagedSuperblockSelection {
1067                copy_index: 0,
1068                generation: 3
1069            })),
1070            1
1071        );
1072        assert_eq!(
1073            paged_superblock_next_copy(Some(PagedSuperblockSelection {
1074                copy_index: 1,
1075                generation: 3
1076            })),
1077            0
1078        );
1079    }
1080
1081    #[test]
1082    fn bit_rot_anywhere_in_the_slot_payload_fails_the_crc() {
1083        let mut slot = vec![0u8; PAGED_SUPERBLOCK_SLOT_SIZE];
1084        init_database_header_page(&mut slot, 3).expect("init header");
1085        seal_paged_superblock_slot(&mut slot, 0, 1).expect("seal slot");
1086
1087        for offset in [
1088            0,
1089            40,
1090            DB_HEADER_MIN_LEN - 1,
1091            PAGED_SUPERBLOCK_SLOT_SIZE - 33,
1092        ] {
1093            let mut rotted = slot.clone();
1094            rotted[offset] ^= 0x01;
1095            assert_eq!(
1096                paged_superblock_slot_generation(&rotted, 0),
1097                None,
1098                "bit rot at offset {offset} must fail the slot CRC"
1099            );
1100        }
1101    }
1102
1103    #[test]
1104    fn paged_page_header_round_trips_raw_layout() {
1105        let header = PagedPageHeader {
1106            page_type: 13,
1107            flags: 0b1010_0001,
1108            cell_count: 42,
1109            free_start: 44,
1110            free_end: 16_000,
1111            page_id: 99,
1112            parent_id: 88,
1113            right_child: 77,
1114            lsn: 66,
1115            checksum: 55,
1116        };
1117
1118        let encoded = encode_paged_page_header(&header);
1119
1120        assert_eq!(encoded.len(), PAGED_PAGE_HEADER_SIZE);
1121        assert_eq!(decode_paged_page_header(&encoded), header);
1122    }
1123
1124    #[test]
1125    fn paged_page_header_accessors_update_expected_offsets() {
1126        let mut page = [0u8; PAGED_PAGE_SIZE];
1127        page[0] = 13;
1128
1129        set_paged_page_cell_count(&mut page, 2);
1130        set_paged_page_free_start(&mut page, 34);
1131        set_paged_page_free_end(&mut page, 4096);
1132        set_paged_page_parent_id(&mut page, 7);
1133        set_paged_page_right_child(&mut page, 8);
1134        set_paged_page_lsn(&mut page, 9);
1135        set_paged_page_checksum(&mut page, 10);
1136
1137        assert_eq!(paged_page_type(&page), 13);
1138        assert_eq!(paged_page_cell_count(&page), 2);
1139        assert_eq!(paged_page_free_start(&page), 34);
1140        assert_eq!(paged_page_free_end(&page), 4096);
1141        assert_eq!(paged_page_parent_id(&page), 7);
1142        assert_eq!(paged_page_right_child(&page), 8);
1143        assert_eq!(paged_page_lsn(&page), 9);
1144        assert_eq!(paged_page_checksum(&page), 10);
1145        clear_paged_page_checksum(&mut page);
1146        assert_eq!(paged_page_checksum(&page), 0);
1147    }
1148
1149    #[test]
1150    fn paged_cell_helpers_round_trip_pointer_and_payload() {
1151        let mut page = [0u8; PAGED_PAGE_SIZE];
1152        let pointer = (PAGED_PAGE_SIZE - 14) as u16;
1153
1154        assert!(write_paged_cell(&mut page, pointer, b"key", b"value"));
1155        assert!(set_paged_cell_pointer(&mut page, 0, pointer));
1156
1157        assert_eq!(paged_cell_pointer(&page, 0), Some(pointer));
1158        let cell = paged_cell_bytes(&page, pointer).expect("cell bytes");
1159        let (key, value) = paged_cell_key_value(cell).expect("key value");
1160
1161        assert_eq!(key, b"key");
1162        assert_eq!(value, b"value");
1163        assert_eq!(paged_cell_total_len(&page, pointer), Some(14));
1164    }
1165
1166    #[test]
1167    fn database_header_field_helpers_preserve_page_zero_offsets() {
1168        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1169
1170        init_database_header_page(&mut page, 7).expect("init database header page");
1171        set_database_header_version(&mut page, PAGE_FILE_VERSION + 1).expect("set version");
1172        set_database_header_page_count(&mut page, 9).expect("set page count");
1173        set_database_header_freelist_head(&mut page, 4).expect("set freelist head");
1174
1175        assert!(database_header_magic_matches(&page));
1176        assert!(matches!(
1177            decode_database_header(&page),
1178            Err(DatabaseHeaderError::UnsupportedDatabaseVersion { .. })
1179        ));
1180        set_database_header_version(&mut page, PAGE_FILE_VERSION).expect("restore version");
1181        assert_eq!(
1182            database_header_page_size(&page).unwrap(),
1183            PAGED_PAGE_SIZE as u32
1184        );
1185        assert_eq!(database_header_page_count(&page).unwrap(), 9);
1186        assert_eq!(database_header_freelist_head(&page).unwrap(), 4);
1187    }
1188
1189    #[test]
1190    fn paged_dwb_frame_round_trips_entries_and_validates_checksum() {
1191        let mut page = [0u8; PAGED_PAGE_SIZE];
1192        page[0] = 7;
1193        let frame = encode_paged_dwb_frame([(42, &page)]);
1194
1195        let entries = decode_paged_dwb_frame(&frame).expect("decode DWB frame");
1196        assert_eq!(entries.len(), 1);
1197        assert_eq!(entries[0].page_id, 42);
1198        assert_eq!(entries[0].page, page);
1199
1200        let mut corrupted = frame;
1201        let last = corrupted.len() - 1;
1202        corrupted[last] ^= 0xFF;
1203        assert!(matches!(
1204            decode_paged_dwb_frame(&corrupted),
1205            Err(PagedDwbFrameError::ChecksumMismatch { .. })
1206        ));
1207    }
1208
1209    #[test]
1210    fn paged_encryption_header_round_trips_marker_and_raw_bytes() {
1211        let header = PagedEncryptionHeader {
1212            salt: [7u8; PAGED_ENCRYPTION_SALT_SIZE],
1213            key_check: vec![9u8; PAGED_ENCRYPTION_KEY_CHECK_BLOB_SIZE],
1214        };
1215        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1216        let bytes = encode_paged_encryption_header(&header);
1217
1218        write_paged_encryption_marker_and_header(&mut page, &bytes)
1219            .expect("write encryption marker");
1220
1221        assert!(paged_encryption_marker_present(&page));
1222        let raw = paged_encryption_header_bytes(&page).expect("header bytes");
1223        assert_eq!(decode_paged_encryption_header(raw).unwrap(), header);
1224    }
1225
1226    #[test]
1227    fn database_header_round_trips_page_zero_contract() {
1228        let header = DatabaseHeader {
1229            version: PAGE_FILE_VERSION,
1230            page_size: PAGED_PAGE_SIZE as u32,
1231            page_count: 42,
1232            freelist_head: 7,
1233            schema_version: 3,
1234            checkpoint_lsn: 99,
1235            checkpoint_in_progress: true,
1236            checkpoint_target_lsn: 123,
1237            physical: PhysicalFileHeader {
1238                format_version: 11,
1239                sequence: 12,
1240                manifest_oldest_root: 13,
1241                manifest_root: 14,
1242                free_set_root: 15,
1243                manifest_page: 16,
1244                manifest_checksum: 17,
1245                collection_roots_page: 18,
1246                collection_roots_checksum: 19,
1247                collection_root_count: 20,
1248                snapshot_count: 21,
1249                index_count: 22,
1250                catalog_collection_count: 23,
1251                catalog_total_entities: 24,
1252                export_count: 25,
1253                graph_projection_count: 26,
1254                analytics_job_count: 27,
1255                manifest_event_count: 28,
1256                registry_page: 29,
1257                registry_checksum: 30,
1258                recovery_page: 31,
1259                recovery_checksum: 32,
1260                catalog_page: 33,
1261                catalog_checksum: 34,
1262                metadata_state_page: 35,
1263                metadata_state_checksum: 36,
1264                vector_artifact_page: 37,
1265                vector_artifact_checksum: 38,
1266            },
1267        };
1268        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1269
1270        encode_database_header(&mut page, &header).expect("encode header");
1271
1272        assert!(database_header_magic_matches(&page));
1273        let decoded = decode_database_header(&page).expect("decode header");
1274        assert_eq!(decoded, header);
1275    }
1276
1277    #[test]
1278    fn database_header_rejects_newer_versions() {
1279        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1280        let header = DatabaseHeader {
1281            version: PAGE_FILE_VERSION + 1,
1282            ..DatabaseHeader::default()
1283        };
1284        encode_database_header(&mut page, &header).expect("encode header");
1285
1286        assert!(matches!(
1287            decode_database_header(&page),
1288            Err(DatabaseHeaderError::UnsupportedDatabaseVersion { .. })
1289        ));
1290    }
1291}