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