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