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