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