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;
30/// The first version whose superblock declares mandatory reader capabilities.
31pub const PAGE_REQUIRED_CAPABILITIES_FILE_VERSION: u32 = 0x0001_0009;
32pub const PAGE_FILE_VERSION: u32 = PAGE_REQUIRED_CAPABILITIES_FILE_VERSION;
33
34/// Native measured-temperature values may appear in entity payloads.
35pub const FILE_CAPABILITY_TEMPERATURE: u64 = 1 << 0;
36/// The file's published trees require a disk-resident writer.
37pub const FILE_CAPABILITY_DISK_RESIDENCY: u64 = 1 << 1;
38/// A copy-on-write catalog is independently sealed by both superblock copies.
39pub const FILE_CAPABILITY_CATALOG_ROOT: u64 = 1 << 2;
40/// Recovery output whose identity high-water marks are unknown cannot admit writers.
41pub const FILE_CAPABILITY_RECOVERY_READ_ONLY: u64 = 1 << 3;
42/// Published catalog roots carry independently sealed tree-image maps.
43pub const FILE_CAPABILITY_TREE_PROOFS: u64 = 1 << 4;
44/// Double-write frames require independent superblock publication authority.
45pub const FILE_CAPABILITY_DWB_PUBLICATION: u64 = 1 << 5;
46// Freeze the old entry point's reader contract. An older engine may link a
47// newer compatible codec crate without implementing its new recovery rules.
48const FILE_CAPABILITIES_LEGACY_HEADER: u64 =
49    FILE_CAPABILITY_TEMPERATURE | FILE_CAPABILITY_DISK_RESIDENCY;
50/// Capabilities understood by this reader. Unknown mandatory bits are fatal.
51pub const FILE_CAPABILITIES_SUPPORTED: u64 = FILE_CAPABILITY_TEMPERATURE
52    | FILE_CAPABILITY_DISK_RESIDENCY
53    | FILE_CAPABILITY_CATALOG_ROOT
54    | FILE_CAPABILITY_RECOVERY_READ_ONLY
55    | FILE_CAPABILITY_TREE_PROOFS
56    | FILE_CAPABILITY_DWB_PUBLICATION;
57
58/// Whether a pre-capability format version implies disk residency.
59/// Capability-aware callers must use [`DatabaseHeader::requires_disk_residency`].
60pub fn page_format_is_disk_resident(version: u32) -> bool {
61    (PAGE_DISK_RESIDENCY_FILE_VERSION..PAGE_REQUIRED_CAPABILITIES_FILE_VERSION).contains(&version)
62        && version != PAGE_RESULT_CACHE_FILE_VERSION
63}
64
65/// Paged database page size.
66pub const PAGED_PAGE_SIZE: usize = 16_384;
67
68/// Paged database page-header size before the page-0 database header.
69pub const PAGED_PAGE_HEADER_SIZE: usize = 32;
70
71pub const PAGED_CELL_POINTER_SIZE: usize = 2;
72pub const PAGED_CELL_HEADER_SIZE: usize = 6;
73
74/// Raw persisted page header encoded at the start of every paged-store page.
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub struct PagedPageHeader {
77    pub page_type: u8,
78    pub flags: u8,
79    pub cell_count: u16,
80    pub free_start: u16,
81    pub free_end: u16,
82    pub page_id: u32,
83    pub parent_id: u32,
84    pub right_child: u32,
85    pub lsn: u64,
86    pub checksum: u32,
87}
88
89pub fn encode_paged_page_header(header: &PagedPageHeader) -> [u8; PAGED_PAGE_HEADER_SIZE] {
90    let mut buf = [0u8; PAGED_PAGE_HEADER_SIZE];
91    buf[0] = header.page_type;
92    buf[1] = header.flags;
93    buf[2..4].copy_from_slice(&header.cell_count.to_le_bytes());
94    buf[4..6].copy_from_slice(&header.free_start.to_le_bytes());
95    buf[6..8].copy_from_slice(&header.free_end.to_le_bytes());
96    buf[8..12].copy_from_slice(&header.page_id.to_le_bytes());
97    buf[12..16].copy_from_slice(&header.parent_id.to_le_bytes());
98    buf[16..20].copy_from_slice(&header.right_child.to_le_bytes());
99    buf[20..28].copy_from_slice(&header.lsn.to_le_bytes());
100    buf[28..32].copy_from_slice(&header.checksum.to_le_bytes());
101    buf
102}
103
104pub fn decode_paged_page_header(buf: &[u8; PAGED_PAGE_HEADER_SIZE]) -> PagedPageHeader {
105    PagedPageHeader {
106        page_type: buf[0],
107        flags: buf[1],
108        cell_count: u16::from_le_bytes([buf[2], buf[3]]),
109        free_start: u16::from_le_bytes([buf[4], buf[5]]),
110        free_end: u16::from_le_bytes([buf[6], buf[7]]),
111        page_id: u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
112        parent_id: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
113        right_child: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
114        lsn: u64::from_le_bytes([
115            buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
116        ]),
117        checksum: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]),
118    }
119}
120
121pub fn paged_page_type(page: &[u8; PAGED_PAGE_SIZE]) -> u8 {
122    page[0]
123}
124
125pub fn paged_page_id(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
126    read_u32(page, 8).expect("paged page has header")
127}
128
129pub fn paged_page_lsn(page: &[u8; PAGED_PAGE_SIZE]) -> u64 {
130    read_u64(page, 20).expect("paged page has header")
131}
132
133pub fn set_paged_page_lsn(page: &mut [u8; PAGED_PAGE_SIZE], lsn: u64) {
134    write_u64(page, 20, lsn).expect("paged page has header");
135}
136
137pub fn paged_page_cell_count(page: &[u8; PAGED_PAGE_SIZE]) -> u16 {
138    read_u16(page, 2).expect("paged page has header")
139}
140
141pub fn set_paged_page_cell_count(page: &mut [u8; PAGED_PAGE_SIZE], count: u16) {
142    write_u16(page, 2, count).expect("paged page has header");
143}
144
145pub fn paged_page_parent_id(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
146    read_u32(page, 12).expect("paged page has header")
147}
148
149pub fn set_paged_page_parent_id(page: &mut [u8; PAGED_PAGE_SIZE], parent_id: u32) {
150    write_u32(page, 12, parent_id).expect("paged page has header");
151}
152
153pub fn paged_page_right_child(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
154    read_u32(page, 16).expect("paged page has header")
155}
156
157pub fn set_paged_page_right_child(page: &mut [u8; PAGED_PAGE_SIZE], child_id: u32) {
158    write_u32(page, 16, child_id).expect("paged page has header");
159}
160
161pub fn paged_page_free_start(page: &[u8; PAGED_PAGE_SIZE]) -> u16 {
162    read_u16(page, 4).expect("paged page has header")
163}
164
165pub fn set_paged_page_free_start(page: &mut [u8; PAGED_PAGE_SIZE], offset: u16) {
166    write_u16(page, 4, offset).expect("paged page has header");
167}
168
169pub fn paged_page_free_end(page: &[u8; PAGED_PAGE_SIZE]) -> u16 {
170    read_u16(page, 6).expect("paged page has header")
171}
172
173pub fn set_paged_page_free_end(page: &mut [u8; PAGED_PAGE_SIZE], offset: u16) {
174    write_u16(page, 6, offset).expect("paged page has header");
175}
176
177pub fn paged_page_checksum(page: &[u8; PAGED_PAGE_SIZE]) -> u32 {
178    read_u32(page, 28).expect("paged page has header")
179}
180
181pub fn clear_paged_page_checksum(page: &mut [u8; PAGED_PAGE_SIZE]) {
182    write_u32(page, 28, 0).expect("paged page has header");
183}
184
185pub fn set_paged_page_checksum(page: &mut [u8; PAGED_PAGE_SIZE], checksum: u32) {
186    write_u32(page, 28, checksum).expect("paged page has header");
187}
188
189pub fn paged_cell_pointer_offset(index: usize) -> Option<usize> {
190    let offset = PAGED_PAGE_HEADER_SIZE.checked_add(index.checked_mul(PAGED_CELL_POINTER_SIZE)?)?;
191    (offset + PAGED_CELL_POINTER_SIZE <= PAGED_PAGE_SIZE).then_some(offset)
192}
193
194pub fn paged_cell_pointer(page: &[u8; PAGED_PAGE_SIZE], index: usize) -> Option<u16> {
195    read_u16(page, paged_cell_pointer_offset(index)?).ok()
196}
197
198pub fn set_paged_cell_pointer(
199    page: &mut [u8; PAGED_PAGE_SIZE],
200    index: usize,
201    pointer: u16,
202) -> bool {
203    let Some(offset) = paged_cell_pointer_offset(index) else {
204        return false;
205    };
206    if !paged_cell_pointer_is_valid(pointer) {
207        return false;
208    }
209    write_u16(page, offset, pointer).is_ok()
210}
211
212pub fn paged_cell_pointer_is_valid(pointer: u16) -> bool {
213    let pointer = pointer as usize;
214    (PAGED_PAGE_HEADER_SIZE..PAGED_PAGE_SIZE).contains(&pointer)
215}
216
217pub fn paged_cell_len(key_len: usize, value_len: usize) -> Option<usize> {
218    PAGED_CELL_HEADER_SIZE
219        .checked_add(key_len)?
220        .checked_add(value_len)
221}
222
223pub fn paged_cell_total_len(page: &[u8; PAGED_PAGE_SIZE], pointer: u16) -> Option<usize> {
224    let pointer = pointer as usize;
225    if pointer + PAGED_CELL_HEADER_SIZE > PAGED_PAGE_SIZE {
226        return None;
227    }
228    let key_len = read_u16(page, pointer).ok()? as usize;
229    let value_len = read_u32(page, pointer + 2).ok()? as usize;
230    let total_len = paged_cell_len(key_len, value_len)?;
231    (pointer + total_len <= PAGED_PAGE_SIZE).then_some(total_len)
232}
233
234pub fn paged_cell_bytes(page: &[u8; PAGED_PAGE_SIZE], pointer: u16) -> Option<&[u8]> {
235    let total_len = paged_cell_total_len(page, pointer)?;
236    let pointer = pointer as usize;
237    Some(&page[pointer..pointer + total_len])
238}
239
240pub fn paged_cell_key_value(cell: &[u8]) -> Option<(&[u8], &[u8])> {
241    if cell.len() < PAGED_CELL_HEADER_SIZE {
242        return None;
243    }
244    let key_len = u16::from_le_bytes(cell[0..2].try_into().ok()?) as usize;
245    let value_len = u32::from_le_bytes(cell[2..6].try_into().ok()?) as usize;
246    let total_len = paged_cell_len(key_len, value_len)?;
247    if total_len > cell.len() {
248        return None;
249    }
250    let key_start = PAGED_CELL_HEADER_SIZE;
251    let value_start = key_start + key_len;
252    Some((
253        &cell[key_start..value_start],
254        &cell[value_start..value_start + value_len],
255    ))
256}
257
258pub fn write_paged_cell(
259    page: &mut [u8; PAGED_PAGE_SIZE],
260    offset: u16,
261    key: &[u8],
262    value: &[u8],
263) -> bool {
264    let Ok(key_len) = u16::try_from(key.len()) else {
265        return false;
266    };
267    let Ok(value_len) = u32::try_from(value.len()) else {
268        return false;
269    };
270    let Some(total_len) = paged_cell_len(key.len(), value.len()) else {
271        return false;
272    };
273    let offset = offset as usize;
274    if offset + total_len > PAGED_PAGE_SIZE {
275        return false;
276    }
277
278    page[offset..offset + 2].copy_from_slice(&key_len.to_le_bytes());
279    page[offset + 2..offset + 6].copy_from_slice(&value_len.to_le_bytes());
280    page[offset + PAGED_CELL_HEADER_SIZE..offset + PAGED_CELL_HEADER_SIZE + key.len()]
281        .copy_from_slice(key);
282    page[offset + PAGED_CELL_HEADER_SIZE + key.len()..offset + total_len].copy_from_slice(value);
283    true
284}
285
286/// Encryption marker embedded in page 0 when page encryption is enabled.
287pub const PAGED_ENCRYPTION_MARKER: [u8; 4] = *b"RDBE";
288
289/// Current page-0 encryption marker offset.
290///
291/// This preserves the existing on-disk placement. It overlaps the historical
292/// physical-header region, so callers must preserve page-0 bytes around normal
293/// header writes.
294pub const PAGED_ENCRYPTION_MARKER_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 32;
295
296pub const PAGED_ENCRYPTION_SALT_SIZE: usize = 32;
297pub const PAGED_ENCRYPTION_KEY_CHECK_PLAINTEXT_SIZE: usize = 32;
298pub const PAGED_ENCRYPTION_KEY_CHECK_BLOB_SIZE: usize = 60;
299pub const PAGED_ENCRYPTION_HEADER_SIZE: usize =
300    PAGED_ENCRYPTION_SALT_SIZE + PAGED_ENCRYPTION_KEY_CHECK_BLOB_SIZE;
301
302#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct PagedEncryptionHeader {
304    pub salt: [u8; PAGED_ENCRYPTION_SALT_SIZE],
305    pub key_check: Vec<u8>,
306}
307
308pub fn encode_paged_encryption_header(header: &PagedEncryptionHeader) -> Vec<u8> {
309    let mut out = Vec::with_capacity(PAGED_ENCRYPTION_HEADER_SIZE);
310    out.extend_from_slice(&header.salt);
311    out.extend_from_slice(&header.key_check);
312    out
313}
314
315pub fn decode_paged_encryption_header(
316    data: &[u8],
317) -> Result<PagedEncryptionHeader, DatabaseHeaderError> {
318    ensure_len(data, PAGED_ENCRYPTION_HEADER_SIZE)?;
319    let mut salt = [0u8; PAGED_ENCRYPTION_SALT_SIZE];
320    salt.copy_from_slice(&data[..PAGED_ENCRYPTION_SALT_SIZE]);
321    let key_check = data[PAGED_ENCRYPTION_SALT_SIZE..PAGED_ENCRYPTION_HEADER_SIZE].to_vec();
322    Ok(PagedEncryptionHeader { salt, key_check })
323}
324
325pub fn paged_encryption_marker_present(page: &[u8]) -> bool {
326    page.get(PAGED_ENCRYPTION_MARKER_OFFSET..PAGED_ENCRYPTION_MARKER_OFFSET + 4)
327        == Some(&PAGED_ENCRYPTION_MARKER)
328}
329
330pub fn paged_encryption_header_bytes(page: &[u8]) -> Option<&[u8]> {
331    let start = PAGED_ENCRYPTION_MARKER_OFFSET + PAGED_ENCRYPTION_MARKER.len();
332    page.get(start..start + PAGED_ENCRYPTION_HEADER_SIZE)
333}
334
335pub fn write_paged_encryption_marker_and_header(
336    page: &mut [u8],
337    header_bytes: &[u8],
338) -> Result<(), DatabaseHeaderError> {
339    let marker_end = PAGED_ENCRYPTION_MARKER_OFFSET + PAGED_ENCRYPTION_MARKER.len();
340    let header_end = marker_end + header_bytes.len();
341    ensure_len(page, header_end)?;
342    page[PAGED_ENCRYPTION_MARKER_OFFSET..marker_end].copy_from_slice(&PAGED_ENCRYPTION_MARKER);
343    page[marker_end..header_end].copy_from_slice(header_bytes);
344    Ok(())
345}
346
347const DB_MAGIC_OFFSET: usize = PAGED_PAGE_HEADER_SIZE;
348const DB_VERSION_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 4;
349const DB_PAGE_SIZE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 8;
350const DB_PAGE_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 12;
351const DB_FREELIST_HEAD_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 16;
352const DB_SCHEMA_VERSION_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 20;
353const DB_CHECKPOINT_LSN_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 24;
354const DB_PHYSICAL_FORMAT_VERSION_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 32;
355const DB_PHYSICAL_SEQUENCE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 36;
356const DB_MANIFEST_ROOT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 44;
357const DB_MANIFEST_OLDEST_ROOT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 52;
358const DB_FREE_SET_ROOT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 60;
359const DB_MANIFEST_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 68;
360const DB_MANIFEST_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 72;
361const DB_COLLECTION_ROOTS_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 80;
362const DB_COLLECTION_ROOTS_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 84;
363const DB_COLLECTION_ROOT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 92;
364const DB_SNAPSHOT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 96;
365const DB_INDEX_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 100;
366const DB_CATALOG_COLLECTION_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 104;
367const DB_CATALOG_TOTAL_ENTITIES_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 108;
368const DB_EXPORT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 116;
369const DB_GRAPH_PROJECTION_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 120;
370const DB_ANALYTICS_JOB_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 124;
371const DB_MANIFEST_EVENT_COUNT_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 128;
372const DB_REGISTRY_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 132;
373const DB_REGISTRY_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 136;
374const DB_RECOVERY_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 144;
375const DB_RECOVERY_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 148;
376const DB_CATALOG_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 156;
377const DB_CATALOG_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 160;
378const DB_METADATA_STATE_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 168;
379const DB_METADATA_STATE_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 172;
380const DB_VECTOR_ARTIFACT_PAGE_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 180;
381const DB_VECTOR_ARTIFACT_CHECKSUM_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 184;
382const DB_CHECKPOINT_IN_PROGRESS_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 192;
383const DB_CHECKPOINT_TARGET_LSN_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 193;
384const DB_REQUIRED_CAPABILITIES_OFFSET: usize = PAGED_PAGE_HEADER_SIZE + 208;
385const DB_HEADER_MIN_LEN: usize = DB_REQUIRED_CAPABILITIES_OFFSET + 8;
386// Keep the legacy encryption envelope (bytes 64..160) and all existing fields intact.
387const DB_MANIFEST_REFERENCE_OFFSET: usize = 256;
388const DB_MANIFEST_REFERENCE_END: usize = DB_MANIFEST_REFERENCE_OFFSET + 36;
389const _: () = assert!(DB_MANIFEST_REFERENCE_END < PAGED_SUPERBLOCK_TRAILER_OFFSET);
390// Maintained by protected page I/O, under the superblock publication lock.
391// Ordinary DatabaseHeader encoding preserves this extension from the newest
392// slot instead of copying stale publication state from the header cache.
393const DB_DWB_PUBLICATION_OFFSET: usize = 296;
394const DB_DWB_PUBLICATION_END: usize = DB_DWB_PUBLICATION_OFFSET + 40;
395const _: () = assert!(DB_MANIFEST_REFERENCE_END <= DB_DWB_PUBLICATION_OFFSET);
396const _: () = assert!(DB_DWB_PUBLICATION_END < PAGED_SUPERBLOCK_TRAILER_OFFSET);
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399#[repr(u32)]
400pub enum DwbPhase {
401    Preparing = 1,
402    Ready = 2,
403    Consumed = 3,
404}
405
406/// Authority over the complete raw frame, including encrypted page images.
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub struct DwbPublication {
409    pub phase: DwbPhase,
410    pub frame_bytes: u32,
411    pub digest: [u8; 32],
412}
413
414pub fn dwb_frame_digest(bytes: &[u8]) -> [u8; 32] {
415    *blake3::hash(bytes).as_bytes()
416}
417
418pub fn database_header_dwb_publication(
419    page: &[u8],
420) -> Result<Option<DwbPublication>, HeaderPublicationError> {
421    ensure_len(page, DB_DWB_PUBLICATION_END)?;
422    let field = &page[DB_DWB_PUBLICATION_OFFSET..DB_DWB_PUBLICATION_END];
423    if field == [0; 40] {
424        return Ok(None);
425    }
426    let phase = match read_u32(field, 0)? {
427        1 => DwbPhase::Preparing,
428        2 => DwbPhase::Ready,
429        3 => DwbPhase::Consumed,
430        _ => return Err(HeaderPublicationError::InvalidDwbPublication),
431    };
432    let frame_bytes = read_u32(field, 4)?;
433    let digest = field[8..40].try_into().expect("fixed digest field");
434    let count = (frame_bytes as usize).checked_sub(PAGED_DWB_HEADER_SIZE);
435    if digest == [0; 32]
436        || !count.is_some_and(|bytes| {
437            bytes >= PAGED_DWB_ENTRY_SIZE
438                && bytes.is_multiple_of(PAGED_DWB_ENTRY_SIZE)
439                && frame_bytes as usize <= 64 * PAGED_PAGE_SIZE
440        })
441    {
442        return Err(HeaderPublicationError::InvalidDwbPublication);
443    }
444    Ok(Some(DwbPublication {
445        phase,
446        frame_bytes,
447        digest,
448    }))
449}
450
451pub fn set_database_header_dwb_publication(
452    page: &mut [u8],
453    publication: Option<DwbPublication>,
454) -> Result<(), HeaderPublicationError> {
455    ensure_len(page, DB_DWB_PUBLICATION_END)?;
456    let field = &mut page[DB_DWB_PUBLICATION_OFFSET..DB_DWB_PUBLICATION_END];
457    field.fill(0);
458    if let Some(publication) = publication {
459        write_u32(field, 0, publication.phase as u32)?;
460        write_u32(field, 4, publication.frame_bytes)?;
461        field[8..40].copy_from_slice(&publication.digest);
462        database_header_dwb_publication(page)?;
463    }
464    Ok(())
465}
466
467/// Double-write-buffer file magic: `RDDW`.
468pub const DWB_MAGIC: [u8; 4] = [0x52, 0x44, 0x44, 0x57];
469pub const PAGED_DWB_HEADER_SIZE: usize = 12;
470pub const PAGED_DWB_ENTRY_HEADER_SIZE: usize = 4;
471pub const PAGED_DWB_ENTRY_SIZE: usize = PAGED_DWB_ENTRY_HEADER_SIZE + PAGED_PAGE_SIZE;
472
473#[derive(Debug, Clone, PartialEq, Eq)]
474pub struct PagedDwbEntry {
475    pub page_id: u32,
476    pub page: [u8; PAGED_PAGE_SIZE],
477}
478
479#[derive(Debug, Clone, PartialEq, Eq)]
480pub enum PagedDwbFrameError {
481    ShortHeader { got: usize },
482    InvalidMagic,
483    IncompleteFrame { expected: usize, got: usize },
484    ChecksumMismatch { expected: u32, actual: u32 },
485}
486
487impl fmt::Display for PagedDwbFrameError {
488    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489        match self {
490            Self::ShortHeader { got } => write!(f, "DWB frame too short: got {got} bytes"),
491            Self::InvalidMagic => write!(f, "invalid DWB frame magic"),
492            Self::IncompleteFrame { expected, got } => {
493                write!(
494                    f,
495                    "incomplete DWB frame: expected {expected} bytes, got {got}"
496                )
497            }
498            Self::ChecksumMismatch { expected, actual } => write!(
499                f,
500                "DWB frame checksum mismatch: expected {expected}, actual {actual}"
501            ),
502        }
503    }
504}
505
506impl std::error::Error for PagedDwbFrameError {}
507
508pub fn encode_paged_dwb_header(count: u32, checksum: u32) -> [u8; PAGED_DWB_HEADER_SIZE] {
509    let mut header = [0; PAGED_DWB_HEADER_SIZE];
510    header[..4].copy_from_slice(&DWB_MAGIC);
511    header[4..8].copy_from_slice(&count.to_le_bytes());
512    header[8..12].copy_from_slice(&checksum.to_le_bytes());
513    header
514}
515
516pub fn decode_paged_dwb_header(bytes: &[u8]) -> Result<(u32, u32), PagedDwbFrameError> {
517    if bytes.len() < PAGED_DWB_HEADER_SIZE {
518        return Err(PagedDwbFrameError::ShortHeader { got: bytes.len() });
519    }
520    if bytes[..4] != DWB_MAGIC {
521        return Err(PagedDwbFrameError::InvalidMagic);
522    }
523    Ok((
524        u32::from_le_bytes(bytes[4..8].try_into().expect("header length checked")),
525        u32::from_le_bytes(bytes[8..12].try_into().expect("header length checked")),
526    ))
527}
528
529pub fn encode_paged_dwb_frame<'a>(
530    pages: impl IntoIterator<Item = (u32, &'a [u8; PAGED_PAGE_SIZE])>,
531) -> Vec<u8> {
532    let pages: Vec<_> = pages.into_iter().collect();
533    let total = PAGED_DWB_HEADER_SIZE + pages.len() * PAGED_DWB_ENTRY_SIZE;
534    let mut out = Vec::with_capacity(total);
535
536    out.extend_from_slice(&encode_paged_dwb_header(
537        u32::try_from(pages.len()).expect("DWB count fits u32"),
538        0,
539    ));
540
541    for (page_id, page) in pages {
542        out.extend_from_slice(&page_id.to_le_bytes());
543        out.extend_from_slice(page);
544    }
545
546    let checksum = crc32(&out[PAGED_DWB_HEADER_SIZE..]);
547    out[8..12].copy_from_slice(&checksum.to_le_bytes());
548    out
549}
550
551pub fn decode_paged_dwb_frame(bytes: &[u8]) -> Result<Vec<PagedDwbEntry>, PagedDwbFrameError> {
552    let (count, stored_checksum) = decode_paged_dwb_header(bytes)?;
553    let count = count as usize;
554    let expected_len = PAGED_DWB_HEADER_SIZE + count * PAGED_DWB_ENTRY_SIZE;
555    if bytes.len() < expected_len {
556        return Err(PagedDwbFrameError::IncompleteFrame {
557            expected: expected_len,
558            got: bytes.len(),
559        });
560    }
561
562    let actual_checksum = crc32(&bytes[PAGED_DWB_HEADER_SIZE..expected_len]);
563    if actual_checksum != stored_checksum {
564        return Err(PagedDwbFrameError::ChecksumMismatch {
565            expected: stored_checksum,
566            actual: actual_checksum,
567        });
568    }
569
570    let mut offset = PAGED_DWB_HEADER_SIZE;
571    let mut entries = Vec::with_capacity(count);
572    for _ in 0..count {
573        let page_id =
574            u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("len checked"));
575        offset += PAGED_DWB_ENTRY_HEADER_SIZE;
576        let mut page = [0u8; PAGED_PAGE_SIZE];
577        page.copy_from_slice(&bytes[offset..offset + PAGED_PAGE_SIZE]);
578        offset += PAGED_PAGE_SIZE;
579        entries.push(PagedDwbEntry { page_id, page });
580    }
581    Ok(entries)
582}
583
584fn crc32(data: &[u8]) -> u32 {
585    crc32fast::hash(data)
586}
587
588/// `b"RDCC"` — RedDB Columnar Chunk. Opens and closes every column block.
589pub const COLUMN_BLOCK_MAGIC: [u8; 4] = *b"RDCC";
590
591/// Column block on-disk format version.
592pub const COLUMN_BLOCK_VERSION_V1: u16 = 1;
593
594/// Reusable segment-level bloom frame v2 magic.
595pub const BLOOM_SEGMENT_V2_MAGIC: u8 = 0xC0;
596
597/// Legacy vector B-tree on-disk page format.
598pub const VECTOR_BTREE_FORMAT_VERSION_V1: u16 = 1;
599
600/// Current vector B-tree on-disk page format.
601pub const VECTOR_BTREE_FORMAT_VERSION_V2: u16 = 2;
602
603/// Vector B-tree format stamped into freshly-written page headers.
604pub const VECTOR_BTREE_FORMAT_VERSION: u16 = VECTOR_BTREE_FORMAT_VERSION_V2;
605
606/// Database file header information persisted in page 0 after the page header.
607#[derive(Debug, Clone, PartialEq, Eq)]
608pub struct DatabaseHeader {
609    pub version: u32,
610    pub page_size: u32,
611    pub page_count: u32,
612    pub freelist_head: u32,
613    pub schema_version: u32,
614    pub checkpoint_lsn: u64,
615    pub checkpoint_in_progress: bool,
616    pub checkpoint_target_lsn: u64,
617    /// Monotonic set of byte-level features required to decode this file.
618    pub required_capabilities: u64,
619    pub physical: PhysicalFileHeader,
620}
621
622impl DatabaseHeader {
623    pub fn is_recovery_read_only(&self) -> bool {
624        self.version == PAGE_RECOVERY_READ_ONLY_FILE_VERSION
625            || self.required_capabilities & FILE_CAPABILITY_RECOVERY_READ_ONLY != 0
626    }
627
628    /// Whether the file requires a writer that participates in the bounded
629    /// disk-residency protocol.
630    ///
631    /// Versions before the capability bitmap encoded that requirement in the
632    /// monotonically increasing version number. Newer files declare it as an
633    /// independent capability so unrelated codecs can evolve the format.
634    pub fn requires_disk_residency(&self) -> bool {
635        if self.version < PAGE_REQUIRED_CAPABILITIES_FILE_VERSION {
636            page_format_is_disk_resident(self.version)
637        } else {
638            self.required_capabilities & FILE_CAPABILITY_DISK_RESIDENCY != 0
639        }
640    }
641}
642
643impl Default for DatabaseHeader {
644    fn default() -> Self {
645        Self {
646            // Prepared disk residency is an explicit host opt-in.
647            version: PAGE_CHECKPOINT_UNDO_FILE_VERSION,
648            page_size: PAGED_PAGE_SIZE as u32,
649            page_count: 1,
650            freelist_head: 0,
651            schema_version: 0,
652            checkpoint_lsn: 0,
653            checkpoint_in_progress: false,
654            checkpoint_target_lsn: 0,
655            required_capabilities: 0,
656            physical: PhysicalFileHeader::default(),
657        }
658    }
659}
660
661/// Minimal physical state mirrored into page 0 for paged databases.
662///
663/// The pager owns when this is read and written, but the field layout is a
664/// durable file compatibility contract.
665#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
666pub struct PhysicalFileHeader {
667    pub format_version: u32,
668    pub sequence: u64,
669    pub manifest_oldest_root: u64,
670    pub manifest_root: u64,
671    pub free_set_root: u64,
672    pub manifest_page: u32,
673    pub manifest_checksum: u64,
674    pub collection_roots_page: u32,
675    pub collection_roots_checksum: u64,
676    pub collection_root_count: u32,
677    pub snapshot_count: u32,
678    pub index_count: u32,
679    pub catalog_collection_count: u32,
680    pub catalog_total_entities: u64,
681    pub export_count: u32,
682    pub graph_projection_count: u32,
683    pub analytics_job_count: u32,
684    pub manifest_event_count: u32,
685    pub registry_page: u32,
686    pub registry_checksum: u64,
687    pub recovery_page: u32,
688    pub recovery_checksum: u64,
689    pub catalog_page: u32,
690    pub catalog_checksum: u64,
691    pub metadata_state_page: u32,
692    pub metadata_state_checksum: u64,
693    pub vector_artifact_page: u32,
694    pub vector_artifact_checksum: u64,
695}
696
697#[derive(Debug, Clone, PartialEq, Eq)]
698pub enum DatabaseHeaderError {
699    ShortPage { need: usize, got: usize },
700    InvalidMagic,
701    UnsupportedPageSize(u32),
702    UnsupportedDatabaseVersion { file_version: u32, supported: u32 },
703    UnsupportedRequiredCapabilities(u64),
704}
705
706impl fmt::Display for DatabaseHeaderError {
707    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
708        match self {
709            Self::ShortPage { need, got } => {
710                write!(f, "database header page too short: need {need} bytes, got {got}")
711            }
712            Self::InvalidMagic => write!(f, "invalid database header magic"),
713            Self::UnsupportedPageSize(size) => write!(f, "Unsupported page size: {size}"),
714            Self::UnsupportedDatabaseVersion {
715                file_version,
716                supported,
717            } => write!(
718                f,
719                "Unsupported database version: file version {file_version} is newer than supported {supported}"
720            ),
721            Self::UnsupportedRequiredCapabilities(bits) => write!(
722                f,
723                "Unsupported required database capabilities: {bits:#018x}"
724            ),
725        }
726    }
727}
728
729impl std::error::Error for DatabaseHeaderError {}
730
731/// Errors in the independently published catalog and recovery extensions.
732/// Keeping these separate preserves exhaustive matches on `DatabaseHeaderError`.
733#[derive(Debug, Clone, PartialEq, Eq)]
734#[non_exhaustive]
735pub enum HeaderPublicationError {
736    Header(DatabaseHeaderError),
737    InvalidManifestReference,
738    InvalidDwbPublication,
739}
740
741impl From<DatabaseHeaderError> for HeaderPublicationError {
742    fn from(error: DatabaseHeaderError) -> Self {
743        Self::Header(error)
744    }
745}
746
747impl fmt::Display for HeaderPublicationError {
748    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749        match self {
750            Self::Header(error) => error.fmt(f),
751            Self::InvalidManifestReference => write!(f, "invalid catalog generation reference"),
752            Self::InvalidDwbPublication => write!(f, "invalid DWB generation publication"),
753        }
754    }
755}
756
757impl std::error::Error for HeaderPublicationError {
758    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
759        match self {
760            Self::Header(error) => Some(error),
761            _ => None,
762        }
763    }
764}
765
766pub fn database_header_magic_matches(page: &[u8]) -> bool {
767    page.get(DB_MAGIC_OFFSET..DB_MAGIC_OFFSET + PAGE_FILE_MAGIC.len()) == Some(&PAGE_FILE_MAGIC)
768}
769
770pub fn init_database_header_page(
771    page: &mut [u8],
772    page_count: u32,
773) -> Result<(), DatabaseHeaderError> {
774    encode_database_header(
775        page,
776        &DatabaseHeader {
777            page_count,
778            ..DatabaseHeader::default()
779        },
780    )
781}
782
783pub fn database_header_page_count(page: &[u8]) -> Result<u32, DatabaseHeaderError> {
784    read_u32(page, DB_PAGE_COUNT_OFFSET)
785}
786
787pub fn set_database_header_version(
788    page: &mut [u8],
789    version: u32,
790) -> Result<(), DatabaseHeaderError> {
791    write_u32(page, DB_VERSION_OFFSET, version)
792}
793
794pub fn set_database_header_page_count(
795    page: &mut [u8],
796    page_count: u32,
797) -> Result<(), DatabaseHeaderError> {
798    write_u32(page, DB_PAGE_COUNT_OFFSET, page_count)
799}
800
801pub fn database_header_freelist_head(page: &[u8]) -> Result<u32, DatabaseHeaderError> {
802    read_u32(page, DB_FREELIST_HEAD_OFFSET)
803}
804
805pub fn set_database_header_freelist_head(
806    page: &mut [u8],
807    page_id: u32,
808) -> Result<(), DatabaseHeaderError> {
809    write_u32(page, DB_FREELIST_HEAD_OFFSET, page_id)
810}
811
812pub fn set_database_header_required_capabilities(
813    page: &mut [u8],
814    capabilities: u64,
815) -> Result<(), DatabaseHeaderError> {
816    write_u64(page, DB_REQUIRED_CAPABILITIES_OFFSET, capabilities)
817}
818
819pub fn set_database_header_manifest_reference(
820    page: &mut [u8],
821    reference: crate::page_proof::PageReference,
822) -> Result<(), HeaderPublicationError> {
823    ensure_len(page, DB_MANIFEST_REFERENCE_END)?;
824    if (reference.page_id == 0) != (reference.digest == [0; 32]) {
825        return Err(HeaderPublicationError::InvalidManifestReference);
826    }
827    crate::page_proof::encode_reference(
828        &mut page[DB_MANIFEST_REFERENCE_OFFSET..DB_MANIFEST_REFERENCE_END],
829        reference,
830    );
831    Ok(())
832}
833
834/// Digest over the catalog root and all continuation pages. The zero reference
835/// retains the legacy fixed page-1 bootstrap catalog. Storage owners validate
836/// this extension before recovery can modify the file.
837pub fn database_header_manifest_reference(
838    page: &[u8],
839) -> Result<crate::page_proof::PageReference, HeaderPublicationError> {
840    let header = decode_database_header_fields(page, FILE_CAPABILITIES_SUPPORTED)?;
841    if header.required_capabilities & FILE_CAPABILITY_CATALOG_ROOT == 0 {
842        return Ok(crate::page_proof::PageReference::default());
843    }
844    ensure_len(page, DB_MANIFEST_REFERENCE_END)?;
845    crate::page_proof::decode_reference(
846        &page[DB_MANIFEST_REFERENCE_OFFSET..DB_MANIFEST_REFERENCE_END],
847    )
848    .ok_or(HeaderPublicationError::InvalidManifestReference)
849}
850
851pub fn database_header_page_size(page: &[u8]) -> Result<u32, DatabaseHeaderError> {
852    read_u32(page, DB_PAGE_SIZE_OFFSET)
853}
854
855/// Decode the legacy header contract. Engines implementing publication-aware
856/// recovery must explicitly opt into [`decode_published_database_header`].
857pub fn decode_database_header(page: &[u8]) -> Result<DatabaseHeader, DatabaseHeaderError> {
858    decode_database_header_fields(page, FILE_CAPABILITIES_LEGACY_HEADER)
859}
860
861/// Validate the ordinary header and its required catalog/recovery publications.
862/// Only storage owners that implement those protocols should use this entry point.
863pub fn decode_published_database_header(
864    page: &[u8],
865) -> Result<DatabaseHeader, HeaderPublicationError> {
866    let header = decode_database_header_fields(page, FILE_CAPABILITIES_SUPPORTED)?;
867    database_header_manifest_reference(page)?;
868    if header.required_capabilities & FILE_CAPABILITY_DWB_PUBLICATION != 0 {
869        database_header_dwb_publication(page)?;
870    }
871    Ok(header)
872}
873
874fn decode_database_header_fields(
875    page: &[u8],
876    supported_capabilities: u64,
877) -> Result<DatabaseHeader, DatabaseHeaderError> {
878    ensure_len(page, DB_HEADER_MIN_LEN)?;
879    if !database_header_magic_matches(page) {
880        return Err(DatabaseHeaderError::InvalidMagic);
881    }
882
883    let version = read_u32(page, DB_VERSION_OFFSET)?;
884    let page_size = read_u32(page, DB_PAGE_SIZE_OFFSET)?;
885    if page_size != PAGED_PAGE_SIZE as u32 {
886        return Err(DatabaseHeaderError::UnsupportedPageSize(page_size));
887    }
888    if version > PAGE_FILE_VERSION {
889        return Err(DatabaseHeaderError::UnsupportedDatabaseVersion {
890            file_version: version,
891            supported: PAGE_FILE_VERSION,
892        });
893    }
894    let required_capabilities = if version >= PAGE_REQUIRED_CAPABILITIES_FILE_VERSION {
895        read_u64(page, DB_REQUIRED_CAPABILITIES_OFFSET)?
896    } else {
897        0
898    };
899    let unsupported_capabilities = required_capabilities & !supported_capabilities;
900    if unsupported_capabilities != 0 {
901        return Err(DatabaseHeaderError::UnsupportedRequiredCapabilities(
902            unsupported_capabilities,
903        ));
904    }
905    Ok(DatabaseHeader {
906        version,
907        page_size,
908        page_count: read_u32(page, DB_PAGE_COUNT_OFFSET)?,
909        freelist_head: read_u32(page, DB_FREELIST_HEAD_OFFSET)?,
910        schema_version: read_u32(page, DB_SCHEMA_VERSION_OFFSET)?,
911        checkpoint_lsn: read_u64(page, DB_CHECKPOINT_LSN_OFFSET)?,
912        checkpoint_in_progress: page[DB_CHECKPOINT_IN_PROGRESS_OFFSET] != 0,
913        checkpoint_target_lsn: read_u64(page, DB_CHECKPOINT_TARGET_LSN_OFFSET)?,
914        required_capabilities,
915        physical: PhysicalFileHeader {
916            format_version: read_u32(page, DB_PHYSICAL_FORMAT_VERSION_OFFSET)?,
917            sequence: read_u64(page, DB_PHYSICAL_SEQUENCE_OFFSET)?,
918            manifest_oldest_root: read_u64(page, DB_MANIFEST_OLDEST_ROOT_OFFSET)?,
919            manifest_root: read_u64(page, DB_MANIFEST_ROOT_OFFSET)?,
920            free_set_root: read_u64(page, DB_FREE_SET_ROOT_OFFSET)?,
921            manifest_page: read_u32(page, DB_MANIFEST_PAGE_OFFSET)?,
922            manifest_checksum: read_u64(page, DB_MANIFEST_CHECKSUM_OFFSET)?,
923            collection_roots_page: read_u32(page, DB_COLLECTION_ROOTS_PAGE_OFFSET)?,
924            collection_roots_checksum: read_u64(page, DB_COLLECTION_ROOTS_CHECKSUM_OFFSET)?,
925            collection_root_count: read_u32(page, DB_COLLECTION_ROOT_COUNT_OFFSET)?,
926            snapshot_count: read_u32(page, DB_SNAPSHOT_COUNT_OFFSET)?,
927            index_count: read_u32(page, DB_INDEX_COUNT_OFFSET)?,
928            catalog_collection_count: read_u32(page, DB_CATALOG_COLLECTION_COUNT_OFFSET)?,
929            catalog_total_entities: read_u64(page, DB_CATALOG_TOTAL_ENTITIES_OFFSET)?,
930            export_count: read_u32(page, DB_EXPORT_COUNT_OFFSET)?,
931            graph_projection_count: read_u32(page, DB_GRAPH_PROJECTION_COUNT_OFFSET)?,
932            analytics_job_count: read_u32(page, DB_ANALYTICS_JOB_COUNT_OFFSET)?,
933            manifest_event_count: read_u32(page, DB_MANIFEST_EVENT_COUNT_OFFSET)?,
934            registry_page: read_u32(page, DB_REGISTRY_PAGE_OFFSET)?,
935            registry_checksum: read_u64(page, DB_REGISTRY_CHECKSUM_OFFSET)?,
936            recovery_page: read_u32(page, DB_RECOVERY_PAGE_OFFSET)?,
937            recovery_checksum: read_u64(page, DB_RECOVERY_CHECKSUM_OFFSET)?,
938            catalog_page: read_u32(page, DB_CATALOG_PAGE_OFFSET)?,
939            catalog_checksum: read_u64(page, DB_CATALOG_CHECKSUM_OFFSET)?,
940            metadata_state_page: read_u32(page, DB_METADATA_STATE_PAGE_OFFSET)?,
941            metadata_state_checksum: read_u64(page, DB_METADATA_STATE_CHECKSUM_OFFSET)?,
942            vector_artifact_page: read_u32(page, DB_VECTOR_ARTIFACT_PAGE_OFFSET)?,
943            vector_artifact_checksum: read_u64(page, DB_VECTOR_ARTIFACT_CHECKSUM_OFFSET)?,
944        },
945    })
946}
947
948pub fn encode_database_header(
949    page: &mut [u8],
950    header: &DatabaseHeader,
951) -> Result<(), DatabaseHeaderError> {
952    ensure_len(page, DB_HEADER_MIN_LEN)?;
953    page[DB_MAGIC_OFFSET..DB_MAGIC_OFFSET + PAGE_FILE_MAGIC.len()]
954        .copy_from_slice(&PAGE_FILE_MAGIC);
955    write_u32(page, DB_VERSION_OFFSET, header.version)?;
956    write_u32(page, DB_PAGE_SIZE_OFFSET, header.page_size)?;
957    write_u32(page, DB_PAGE_COUNT_OFFSET, header.page_count)?;
958    write_u32(page, DB_FREELIST_HEAD_OFFSET, header.freelist_head)?;
959    write_u32(page, DB_SCHEMA_VERSION_OFFSET, header.schema_version)?;
960    write_u64(page, DB_CHECKPOINT_LSN_OFFSET, header.checkpoint_lsn)?;
961    write_u32(
962        page,
963        DB_PHYSICAL_FORMAT_VERSION_OFFSET,
964        header.physical.format_version,
965    )?;
966    write_u64(page, DB_PHYSICAL_SEQUENCE_OFFSET, header.physical.sequence)?;
967    write_u64(page, DB_MANIFEST_ROOT_OFFSET, header.physical.manifest_root)?;
968    write_u64(
969        page,
970        DB_MANIFEST_OLDEST_ROOT_OFFSET,
971        header.physical.manifest_oldest_root,
972    )?;
973    write_u64(page, DB_FREE_SET_ROOT_OFFSET, header.physical.free_set_root)?;
974    write_u32(page, DB_MANIFEST_PAGE_OFFSET, header.physical.manifest_page)?;
975    write_u64(
976        page,
977        DB_MANIFEST_CHECKSUM_OFFSET,
978        header.physical.manifest_checksum,
979    )?;
980    write_u32(
981        page,
982        DB_COLLECTION_ROOTS_PAGE_OFFSET,
983        header.physical.collection_roots_page,
984    )?;
985    write_u64(
986        page,
987        DB_COLLECTION_ROOTS_CHECKSUM_OFFSET,
988        header.physical.collection_roots_checksum,
989    )?;
990    write_u32(
991        page,
992        DB_COLLECTION_ROOT_COUNT_OFFSET,
993        header.physical.collection_root_count,
994    )?;
995    write_u32(
996        page,
997        DB_SNAPSHOT_COUNT_OFFSET,
998        header.physical.snapshot_count,
999    )?;
1000    write_u32(page, DB_INDEX_COUNT_OFFSET, header.physical.index_count)?;
1001    write_u32(
1002        page,
1003        DB_CATALOG_COLLECTION_COUNT_OFFSET,
1004        header.physical.catalog_collection_count,
1005    )?;
1006    write_u64(
1007        page,
1008        DB_CATALOG_TOTAL_ENTITIES_OFFSET,
1009        header.physical.catalog_total_entities,
1010    )?;
1011    write_u32(page, DB_EXPORT_COUNT_OFFSET, header.physical.export_count)?;
1012    write_u32(
1013        page,
1014        DB_GRAPH_PROJECTION_COUNT_OFFSET,
1015        header.physical.graph_projection_count,
1016    )?;
1017    write_u32(
1018        page,
1019        DB_ANALYTICS_JOB_COUNT_OFFSET,
1020        header.physical.analytics_job_count,
1021    )?;
1022    write_u32(
1023        page,
1024        DB_MANIFEST_EVENT_COUNT_OFFSET,
1025        header.physical.manifest_event_count,
1026    )?;
1027    write_u32(page, DB_REGISTRY_PAGE_OFFSET, header.physical.registry_page)?;
1028    write_u64(
1029        page,
1030        DB_REGISTRY_CHECKSUM_OFFSET,
1031        header.physical.registry_checksum,
1032    )?;
1033    write_u32(page, DB_RECOVERY_PAGE_OFFSET, header.physical.recovery_page)?;
1034    write_u64(
1035        page,
1036        DB_RECOVERY_CHECKSUM_OFFSET,
1037        header.physical.recovery_checksum,
1038    )?;
1039    write_u32(page, DB_CATALOG_PAGE_OFFSET, header.physical.catalog_page)?;
1040    write_u64(
1041        page,
1042        DB_CATALOG_CHECKSUM_OFFSET,
1043        header.physical.catalog_checksum,
1044    )?;
1045    write_u32(
1046        page,
1047        DB_METADATA_STATE_PAGE_OFFSET,
1048        header.physical.metadata_state_page,
1049    )?;
1050    write_u64(
1051        page,
1052        DB_METADATA_STATE_CHECKSUM_OFFSET,
1053        header.physical.metadata_state_checksum,
1054    )?;
1055    write_u32(
1056        page,
1057        DB_VECTOR_ARTIFACT_PAGE_OFFSET,
1058        header.physical.vector_artifact_page,
1059    )?;
1060    write_u64(
1061        page,
1062        DB_VECTOR_ARTIFACT_CHECKSUM_OFFSET,
1063        header.physical.vector_artifact_checksum,
1064    )?;
1065    page[DB_CHECKPOINT_IN_PROGRESS_OFFSET] = if header.checkpoint_in_progress { 1 } else { 0 };
1066    write_u64(
1067        page,
1068        DB_CHECKPOINT_TARGET_LSN_OFFSET,
1069        header.checkpoint_target_lsn,
1070    )?;
1071    write_u64(
1072        page,
1073        DB_REQUIRED_CAPABILITIES_OFFSET,
1074        header.required_capabilities,
1075    )?;
1076    // Independently published catalog and DWB authority carry forward from the
1077    // newest slot; an ordinary header update must not replace either extension.
1078    Ok(())
1079}
1080
1081// ── Paged superblock zone (ADR 0038 §2 phase 1) ─────────────────────────
1082//
1083// The first two sectors of a paged `.rdb` are the superblock zone: a
1084// ping-pong pair of fixed-size slots. Each slot holds the page-0 image (page
1085// header + database header + encryption header) followed by a trailer that
1086// carries the slot's copy index, generation counter and CRC.
1087//
1088// An update always writes the *older* slot and fsyncs before it becomes the
1089// newest valid copy, so a crash mid-write can only ever damage the copy that
1090// was already stale. `select_paged_superblock` picks the highest generation
1091// whose CRC verifies; both invalid means the store does not open.
1092//
1093// The zone lives inside page 0's 16 KiB, whose tail was unused. Page 0 is
1094// therefore no longer a checksummed `Page` — the slot CRCs are its integrity
1095// contract.
1096
1097/// Bytes per superblock slot. Sector-aligned so one slot write never touches
1098/// its sibling.
1099pub const PAGED_SUPERBLOCK_SLOT_SIZE: usize = 4096;
1100
1101/// The ping-pong pair.
1102pub const PAGED_SUPERBLOCK_SLOT_COUNT: usize = 2;
1103
1104/// Total bytes reserved for the superblock zone at the head of the file.
1105pub const PAGED_SUPERBLOCK_ZONE_SIZE: usize =
1106    PAGED_SUPERBLOCK_SLOT_SIZE * PAGED_SUPERBLOCK_SLOT_COUNT;
1107
1108const PAGED_SUPERBLOCK_MAGIC: [u8; 8] = *b"RDBPSBK1";
1109const PAGED_SUPERBLOCK_SLOT_VERSION: u32 = 1;
1110
1111/// Offset of the trailer within a slot: magic(8) ‖ version(4) ‖ copy(4) ‖
1112/// generation(8) ‖ crc(4) ‖ reserved(4). Everything before it is the page-0
1113/// image, which is why the offline migration tool can strip the trailer to
1114/// recover the sidecar-era page 0 byte for byte.
1115pub const PAGED_SUPERBLOCK_TRAILER_OFFSET: usize = PAGED_SUPERBLOCK_SLOT_SIZE - 32;
1116const PAGED_SUPERBLOCK_VERSION_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 8;
1117const PAGED_SUPERBLOCK_COPY_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 12;
1118const PAGED_SUPERBLOCK_GENERATION_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 16;
1119const PAGED_SUPERBLOCK_CRC_OFFSET: usize = PAGED_SUPERBLOCK_TRAILER_OFFSET + 24;
1120
1121/// The newest valid copy found in a superblock zone.
1122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1123pub struct PagedSuperblockSelection {
1124    pub copy_index: usize,
1125    pub generation: u64,
1126}
1127
1128/// Byte offset of `copy_index`'s slot from the start of the file.
1129pub fn paged_superblock_slot_offset(copy_index: usize) -> u64 {
1130    (copy_index % PAGED_SUPERBLOCK_SLOT_COUNT) as u64 * PAGED_SUPERBLOCK_SLOT_SIZE as u64
1131}
1132
1133/// The copy a fresh update must target: always the one that is *not* newest,
1134/// so the newest durable copy survives a crash during the write.
1135pub fn paged_superblock_next_copy(newest: Option<PagedSuperblockSelection>) -> usize {
1136    match newest {
1137        Some(selection) => (selection.copy_index + 1) % PAGED_SUPERBLOCK_SLOT_COUNT,
1138        None => 0,
1139    }
1140}
1141
1142/// Stamp `slot`'s trailer (copy index, generation) and seal it with a CRC over
1143/// everything that precedes the checksum field.
1144pub fn seal_paged_superblock_slot(
1145    slot: &mut [u8],
1146    copy_index: usize,
1147    generation: u64,
1148) -> Result<(), DatabaseHeaderError> {
1149    ensure_len(slot, PAGED_SUPERBLOCK_SLOT_SIZE)?;
1150    slot[PAGED_SUPERBLOCK_TRAILER_OFFSET..PAGED_SUPERBLOCK_TRAILER_OFFSET + 8]
1151        .copy_from_slice(&PAGED_SUPERBLOCK_MAGIC);
1152    write_u32(
1153        slot,
1154        PAGED_SUPERBLOCK_VERSION_OFFSET,
1155        PAGED_SUPERBLOCK_SLOT_VERSION,
1156    )?;
1157    write_u32(
1158        slot,
1159        PAGED_SUPERBLOCK_COPY_OFFSET,
1160        u32::try_from(copy_index).unwrap_or(u32::MAX),
1161    )?;
1162    write_u64(slot, PAGED_SUPERBLOCK_GENERATION_OFFSET, generation)?;
1163    let crc = paged_superblock_crc(&slot[..PAGED_SUPERBLOCK_CRC_OFFSET]);
1164    write_u32(slot, PAGED_SUPERBLOCK_CRC_OFFSET, crc)?;
1165    // The 4 trailing reserved bytes stay zero.
1166    write_u32(slot, PAGED_SUPERBLOCK_CRC_OFFSET + 4, 0)
1167}
1168
1169/// Decode `slot`'s generation, or `None` when the slot was never written, is
1170/// torn, carries a foreign copy index, or fails its CRC.
1171pub fn paged_superblock_slot_generation(slot: &[u8], copy_index: usize) -> Option<u64> {
1172    if slot.len() < PAGED_SUPERBLOCK_SLOT_SIZE {
1173        return None;
1174    }
1175    if slot[PAGED_SUPERBLOCK_TRAILER_OFFSET..PAGED_SUPERBLOCK_TRAILER_OFFSET + 8]
1176        != PAGED_SUPERBLOCK_MAGIC
1177    {
1178        return None;
1179    }
1180    if read_u32(slot, PAGED_SUPERBLOCK_VERSION_OFFSET).ok()? != PAGED_SUPERBLOCK_SLOT_VERSION {
1181        return None;
1182    }
1183    // A slot that names a different copy is a misdirected write, not a
1184    // legitimately older generation.
1185    if read_u32(slot, PAGED_SUPERBLOCK_COPY_OFFSET).ok()? != u32::try_from(copy_index).ok()? {
1186        return None;
1187    }
1188    let stored = read_u32(slot, PAGED_SUPERBLOCK_CRC_OFFSET).ok()?;
1189    if paged_superblock_crc(&slot[..PAGED_SUPERBLOCK_CRC_OFFSET]) != stored {
1190        return None;
1191    }
1192    read_u64(slot, PAGED_SUPERBLOCK_GENERATION_OFFSET).ok()
1193}
1194
1195/// Pick the newest valid copy in a full superblock zone. `None` means both
1196/// copies are invalid: the caller must refuse to open the store.
1197pub fn select_paged_superblock(zone: &[u8]) -> Option<PagedSuperblockSelection> {
1198    (0..PAGED_SUPERBLOCK_SLOT_COUNT)
1199        .filter_map(|copy_index| {
1200            let start = copy_index * PAGED_SUPERBLOCK_SLOT_SIZE;
1201            let slot = zone.get(start..start + PAGED_SUPERBLOCK_SLOT_SIZE)?;
1202            Some(PagedSuperblockSelection {
1203                copy_index,
1204                generation: paged_superblock_slot_generation(slot, copy_index)?,
1205            })
1206        })
1207        .max_by_key(|selection| selection.generation)
1208}
1209
1210fn paged_superblock_crc(bytes: &[u8]) -> u32 {
1211    let mut hasher = crc32fast::Hasher::new();
1212    hasher.update(bytes);
1213    hasher.finalize()
1214}
1215
1216fn ensure_len(bytes: &[u8], need: usize) -> Result<(), DatabaseHeaderError> {
1217    if bytes.len() < need {
1218        return Err(DatabaseHeaderError::ShortPage {
1219            need,
1220            got: bytes.len(),
1221        });
1222    }
1223    Ok(())
1224}
1225
1226fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, DatabaseHeaderError> {
1227    ensure_len(bytes, offset + 4)?;
1228    Ok(u32::from_le_bytes(
1229        bytes[offset..offset + 4].try_into().expect("len checked"),
1230    ))
1231}
1232
1233fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, DatabaseHeaderError> {
1234    ensure_len(bytes, offset + 2)?;
1235    Ok(u16::from_le_bytes(
1236        bytes[offset..offset + 2].try_into().expect("len checked"),
1237    ))
1238}
1239
1240fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, DatabaseHeaderError> {
1241    ensure_len(bytes, offset + 8)?;
1242    Ok(u64::from_le_bytes(
1243        bytes[offset..offset + 8].try_into().expect("len checked"),
1244    ))
1245}
1246
1247fn write_u32(bytes: &mut [u8], offset: usize, value: u32) -> Result<(), DatabaseHeaderError> {
1248    ensure_len(bytes, offset + 4)?;
1249    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1250    Ok(())
1251}
1252
1253fn write_u16(bytes: &mut [u8], offset: usize, value: u16) -> Result<(), DatabaseHeaderError> {
1254    ensure_len(bytes, offset + 2)?;
1255    bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
1256    Ok(())
1257}
1258
1259fn write_u64(bytes: &mut [u8], offset: usize, value: u64) -> Result<(), DatabaseHeaderError> {
1260    ensure_len(bytes, offset + 8)?;
1261    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1262    Ok(())
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267    use super::*;
1268
1269    fn sealed_zone(gen_a: Option<u64>, gen_b: Option<u64>) -> Vec<u8> {
1270        let mut zone = vec![0u8; PAGED_SUPERBLOCK_ZONE_SIZE];
1271        for (copy_index, generation) in [gen_a, gen_b].into_iter().enumerate() {
1272            let Some(generation) = generation else {
1273                continue;
1274            };
1275            let start = copy_index * PAGED_SUPERBLOCK_SLOT_SIZE;
1276            let slot = &mut zone[start..start + PAGED_SUPERBLOCK_SLOT_SIZE];
1277            init_database_header_page(slot, 3).expect("init header");
1278            seal_paged_superblock_slot(slot, copy_index, generation).expect("seal slot");
1279        }
1280        zone
1281    }
1282
1283    #[test]
1284    fn superblock_zone_fits_two_sector_aligned_slots_before_page_one() {
1285        assert_eq!(PAGED_SUPERBLOCK_SLOT_SIZE, 4096);
1286        assert_eq!(PAGED_SUPERBLOCK_ZONE_SIZE, 8192);
1287        // Both slots live inside page 0, so page 1 is never touched by a
1288        // superblock update.
1289        assert!(PAGED_SUPERBLOCK_ZONE_SIZE <= PAGED_PAGE_SIZE);
1290        // The database header must not overrun into the slot trailer.
1291        assert!(DB_HEADER_MIN_LEN < PAGED_SUPERBLOCK_TRAILER_OFFSET);
1292        assert_eq!(paged_superblock_slot_offset(0), 0);
1293        assert_eq!(paged_superblock_slot_offset(1), 4096);
1294    }
1295
1296    #[test]
1297    fn sealed_slot_round_trips_generation_and_keeps_the_header_readable() {
1298        let mut slot = vec![0u8; PAGED_SUPERBLOCK_SLOT_SIZE];
1299        init_database_header_page(&mut slot, 7).expect("init header");
1300        seal_paged_superblock_slot(&mut slot, 1, 42).expect("seal slot");
1301
1302        assert_eq!(paged_superblock_slot_generation(&slot, 1), Some(42));
1303        assert_eq!(
1304            database_header_page_count(&slot).expect("database_header_page_count() should succeed"),
1305            7
1306        );
1307    }
1308
1309    #[test]
1310    fn selection_prefers_the_newest_valid_copy_and_survives_one_torn_slot() {
1311        let zone = sealed_zone(Some(4), Some(5));
1312        assert_eq!(
1313            select_paged_superblock(&zone),
1314            Some(PagedSuperblockSelection {
1315                copy_index: 1,
1316                generation: 5
1317            })
1318        );
1319
1320        // Tear the newer copy: open falls back to the older intact one.
1321        let mut torn = zone.clone();
1322        torn[PAGED_SUPERBLOCK_SLOT_SIZE + 64] ^= 0xFF;
1323        assert_eq!(
1324            select_paged_superblock(&torn),
1325            Some(PagedSuperblockSelection {
1326                copy_index: 0,
1327                generation: 4
1328            })
1329        );
1330    }
1331
1332    #[test]
1333    fn a_misdirected_slot_copy_is_rejected_not_read_as_an_older_generation() {
1334        // Slot 0's bytes written at slot 1's offset: right data, wrong place.
1335        let zone = sealed_zone(Some(9), None);
1336        let (slot_zero, _) = zone.split_at(PAGED_SUPERBLOCK_SLOT_SIZE);
1337        assert_eq!(paged_superblock_slot_generation(slot_zero, 1), None);
1338    }
1339
1340    #[test]
1341    fn both_copies_invalid_yields_no_selection() {
1342        assert_eq!(
1343            select_paged_superblock(&vec![0u8; PAGED_SUPERBLOCK_ZONE_SIZE]),
1344            None
1345        );
1346        assert_eq!(select_paged_superblock(&[]), None);
1347
1348        let mut zone = sealed_zone(Some(1), Some(2));
1349        zone[64] ^= 0xFF;
1350        zone[PAGED_SUPERBLOCK_SLOT_SIZE + 64] ^= 0xFF;
1351        assert_eq!(select_paged_superblock(&zone), None);
1352    }
1353
1354    #[test]
1355    fn the_next_copy_to_write_is_always_the_stale_one() {
1356        assert_eq!(paged_superblock_next_copy(None), 0);
1357        assert_eq!(
1358            paged_superblock_next_copy(Some(PagedSuperblockSelection {
1359                copy_index: 0,
1360                generation: 3
1361            })),
1362            1
1363        );
1364        assert_eq!(
1365            paged_superblock_next_copy(Some(PagedSuperblockSelection {
1366                copy_index: 1,
1367                generation: 3
1368            })),
1369            0
1370        );
1371    }
1372
1373    #[test]
1374    fn bit_rot_anywhere_in_the_slot_payload_fails_the_crc() {
1375        let mut slot = vec![0u8; PAGED_SUPERBLOCK_SLOT_SIZE];
1376        init_database_header_page(&mut slot, 3).expect("init header");
1377        seal_paged_superblock_slot(&mut slot, 0, 1).expect("seal slot");
1378
1379        for offset in [
1380            0,
1381            40,
1382            DB_HEADER_MIN_LEN - 1,
1383            PAGED_SUPERBLOCK_SLOT_SIZE - 33,
1384        ] {
1385            let mut rotted = slot.clone();
1386            rotted[offset] ^= 0x01;
1387            assert_eq!(
1388                paged_superblock_slot_generation(&rotted, 0),
1389                None,
1390                "bit rot at offset {offset} must fail the slot CRC"
1391            );
1392        }
1393    }
1394
1395    #[test]
1396    fn paged_page_header_round_trips_raw_layout() {
1397        let header = PagedPageHeader {
1398            page_type: 13,
1399            flags: 0b1010_0001,
1400            cell_count: 42,
1401            free_start: 44,
1402            free_end: 16_000,
1403            page_id: 99,
1404            parent_id: 88,
1405            right_child: 77,
1406            lsn: 66,
1407            checksum: 55,
1408        };
1409
1410        let encoded = encode_paged_page_header(&header);
1411
1412        assert_eq!(encoded.len(), PAGED_PAGE_HEADER_SIZE);
1413        assert_eq!(decode_paged_page_header(&encoded), header);
1414    }
1415
1416    #[test]
1417    fn paged_page_header_accessors_update_expected_offsets() {
1418        let mut page = [0u8; PAGED_PAGE_SIZE];
1419        page[0] = 13;
1420
1421        set_paged_page_cell_count(&mut page, 2);
1422        set_paged_page_free_start(&mut page, 34);
1423        set_paged_page_free_end(&mut page, 4096);
1424        set_paged_page_parent_id(&mut page, 7);
1425        set_paged_page_right_child(&mut page, 8);
1426        set_paged_page_lsn(&mut page, 9);
1427        set_paged_page_checksum(&mut page, 10);
1428
1429        assert_eq!(paged_page_type(&page), 13);
1430        assert_eq!(paged_page_cell_count(&page), 2);
1431        assert_eq!(paged_page_free_start(&page), 34);
1432        assert_eq!(paged_page_free_end(&page), 4096);
1433        assert_eq!(paged_page_parent_id(&page), 7);
1434        assert_eq!(paged_page_right_child(&page), 8);
1435        assert_eq!(paged_page_lsn(&page), 9);
1436        assert_eq!(paged_page_checksum(&page), 10);
1437        clear_paged_page_checksum(&mut page);
1438        assert_eq!(paged_page_checksum(&page), 0);
1439    }
1440
1441    #[test]
1442    fn paged_cell_helpers_round_trip_pointer_and_payload() {
1443        let mut page = [0u8; PAGED_PAGE_SIZE];
1444        let pointer = (PAGED_PAGE_SIZE - 14) as u16;
1445
1446        assert!(write_paged_cell(&mut page, pointer, b"key", b"value"));
1447        assert!(set_paged_cell_pointer(&mut page, 0, pointer));
1448
1449        assert_eq!(paged_cell_pointer(&page, 0), Some(pointer));
1450        let cell = paged_cell_bytes(&page, pointer).expect("cell bytes");
1451        let (key, value) = paged_cell_key_value(cell).expect("key value");
1452
1453        assert_eq!(key, b"key");
1454        assert_eq!(value, b"value");
1455        assert_eq!(paged_cell_total_len(&page, pointer), Some(14));
1456    }
1457
1458    #[test]
1459    fn database_header_field_helpers_preserve_page_zero_offsets() {
1460        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1461
1462        init_database_header_page(&mut page, 7).expect("init database header page");
1463        set_database_header_version(&mut page, PAGE_FILE_VERSION + 1).expect("set version");
1464        set_database_header_page_count(&mut page, 9).expect("set page count");
1465        set_database_header_freelist_head(&mut page, 4).expect("set freelist head");
1466
1467        assert!(database_header_magic_matches(&page));
1468        assert!(matches!(
1469            decode_database_header(&page),
1470            Err(DatabaseHeaderError::UnsupportedDatabaseVersion { .. })
1471        ));
1472        set_database_header_version(&mut page, PAGE_FILE_VERSION).expect("restore version");
1473        assert_eq!(
1474            database_header_page_size(&page).unwrap(),
1475            PAGED_PAGE_SIZE as u32
1476        );
1477        assert_eq!(database_header_page_count(&page).unwrap(), 9);
1478        assert_eq!(database_header_freelist_head(&page).unwrap(), 4);
1479    }
1480
1481    #[test]
1482    fn required_capabilities_round_trip_and_unknown_bits_fail_before_page_reads() {
1483        let mut page = vec![0u8; PAGED_SUPERBLOCK_SLOT_SIZE];
1484        let header = DatabaseHeader {
1485            version: PAGE_REQUIRED_CAPABILITIES_FILE_VERSION,
1486            required_capabilities: FILE_CAPABILITY_TEMPERATURE,
1487            ..DatabaseHeader::default()
1488        };
1489        encode_database_header(&mut page, &header).expect("encode capabilities");
1490        assert_eq!(
1491            decode_database_header(&page)
1492                .expect("supported capabilities")
1493                .required_capabilities,
1494            FILE_CAPABILITY_TEMPERATURE
1495        );
1496
1497        set_database_header_required_capabilities(&mut page, 1 << 63)
1498            .expect("set unknown capability");
1499        assert_eq!(
1500            decode_database_header(&page),
1501            Err(DatabaseHeaderError::UnsupportedRequiredCapabilities(
1502                1 << 63
1503            ))
1504        );
1505    }
1506
1507    #[test]
1508    fn disk_residency_moves_from_legacy_version_to_explicit_capability() {
1509        let legacy = DatabaseHeader {
1510            version: PAGE_DISK_RESIDENCY_FILE_VERSION,
1511            ..DatabaseHeader::default()
1512        };
1513        assert!(legacy.requires_disk_residency());
1514
1515        let resident_result_cache = DatabaseHeader {
1516            version: PAGE_RESULT_CACHE_FILE_VERSION,
1517            ..DatabaseHeader::default()
1518        };
1519        assert!(!resident_result_cache.requires_disk_residency());
1520
1521        let disk_result_cache = DatabaseHeader {
1522            version: PAGE_RESULT_CACHE_DISK_FILE_VERSION,
1523            ..DatabaseHeader::default()
1524        };
1525        assert!(disk_result_cache.requires_disk_residency());
1526
1527        let temperature_only = DatabaseHeader {
1528            version: PAGE_REQUIRED_CAPABILITIES_FILE_VERSION,
1529            required_capabilities: FILE_CAPABILITY_TEMPERATURE,
1530            ..DatabaseHeader::default()
1531        };
1532        assert!(!temperature_only.requires_disk_residency());
1533
1534        let disk_resident = DatabaseHeader {
1535            required_capabilities: FILE_CAPABILITY_DISK_RESIDENCY,
1536            ..temperature_only
1537        };
1538        assert!(disk_resident.requires_disk_residency());
1539    }
1540
1541    #[test]
1542    fn legacy_headers_ignore_the_reserved_capability_bytes() {
1543        let mut page = vec![0u8; PAGED_SUPERBLOCK_SLOT_SIZE];
1544        init_database_header_page(&mut page, 7).expect("init header");
1545        set_database_header_required_capabilities(&mut page, u64::MAX)
1546            .expect("fill reserved bytes");
1547
1548        assert_eq!(
1549            decode_database_header(&page)
1550                .expect("legacy header")
1551                .required_capabilities,
1552            0
1553        );
1554    }
1555
1556    #[test]
1557    fn paged_dwb_frame_round_trips_entries_and_validates_checksum() {
1558        let mut page = [0u8; PAGED_PAGE_SIZE];
1559        page[0] = 7;
1560        let frame = encode_paged_dwb_frame([(42, &page)]);
1561
1562        let entries = decode_paged_dwb_frame(&frame).expect("decode DWB frame");
1563        assert_eq!(entries.len(), 1);
1564        assert_eq!(entries[0].page_id, 42);
1565        assert_eq!(entries[0].page, page);
1566
1567        let mut corrupted = frame;
1568        let last = corrupted.len() - 1;
1569        corrupted[last] ^= 0xFF;
1570        assert!(matches!(
1571            decode_paged_dwb_frame(&corrupted),
1572            Err(PagedDwbFrameError::ChecksumMismatch { .. })
1573        ));
1574    }
1575
1576    #[test]
1577    fn paged_encryption_header_round_trips_marker_and_raw_bytes() {
1578        let header = PagedEncryptionHeader {
1579            salt: [7u8; PAGED_ENCRYPTION_SALT_SIZE],
1580            key_check: vec![9u8; PAGED_ENCRYPTION_KEY_CHECK_BLOB_SIZE],
1581        };
1582        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1583        let bytes = encode_paged_encryption_header(&header);
1584
1585        write_paged_encryption_marker_and_header(&mut page, &bytes)
1586            .expect("write encryption marker");
1587
1588        assert!(paged_encryption_marker_present(&page));
1589        let raw = paged_encryption_header_bytes(&page).expect("header bytes");
1590        assert_eq!(decode_paged_encryption_header(raw).unwrap(), header);
1591    }
1592
1593    #[test]
1594    fn database_header_round_trips_page_zero_contract() {
1595        let header = DatabaseHeader {
1596            version: PAGE_FILE_VERSION,
1597            page_size: PAGED_PAGE_SIZE as u32,
1598            page_count: 42,
1599            freelist_head: 7,
1600            schema_version: 3,
1601            checkpoint_lsn: 99,
1602            checkpoint_in_progress: true,
1603            checkpoint_target_lsn: 123,
1604            required_capabilities: FILE_CAPABILITY_TEMPERATURE,
1605            physical: PhysicalFileHeader {
1606                format_version: 11,
1607                sequence: 12,
1608                manifest_oldest_root: 13,
1609                manifest_root: 14,
1610                free_set_root: 15,
1611                manifest_page: 16,
1612                manifest_checksum: 17,
1613                collection_roots_page: 18,
1614                collection_roots_checksum: 19,
1615                collection_root_count: 20,
1616                snapshot_count: 21,
1617                index_count: 22,
1618                catalog_collection_count: 23,
1619                catalog_total_entities: 24,
1620                export_count: 25,
1621                graph_projection_count: 26,
1622                analytics_job_count: 27,
1623                manifest_event_count: 28,
1624                registry_page: 29,
1625                registry_checksum: 30,
1626                recovery_page: 31,
1627                recovery_checksum: 32,
1628                catalog_page: 33,
1629                catalog_checksum: 34,
1630                metadata_state_page: 35,
1631                metadata_state_checksum: 36,
1632                vector_artifact_page: 37,
1633                vector_artifact_checksum: 38,
1634            },
1635        };
1636        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1637
1638        encode_database_header(&mut page, &header).expect("encode header");
1639
1640        assert!(database_header_magic_matches(&page));
1641        let decoded = decode_database_header(&page).expect("decode header");
1642        assert_eq!(decoded, header);
1643    }
1644
1645    #[test]
1646    fn database_header_rejects_newer_versions() {
1647        let mut page = vec![0u8; PAGED_PAGE_SIZE];
1648        let header = DatabaseHeader {
1649            version: PAGE_FILE_VERSION + 1,
1650            ..DatabaseHeader::default()
1651        };
1652        encode_database_header(&mut page, &header).expect("encode header");
1653
1654        assert!(matches!(
1655            decode_database_header(&page),
1656            Err(DatabaseHeaderError::UnsupportedDatabaseVersion { .. })
1657        ));
1658    }
1659}