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