1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
// page.rs — Foundation layer (layer 1). Defines the on-disk page format:
// page size, type tags, header sizes, magic/format-version constants, and
// the checksum primitives every other page module relies on.
//
// Invariant: every page on disk is exactly PAGE_SIZE bytes and ends with an
// 8-byte little-endian XXH3 checksum computed over bytes 0..CHECKSUM_OFFSET.
// PageCache validates this checksum on every disk LOAD (cache miss); cache
// hits skip revalidation because the in-memory bytes are trusted between
// writes. A mismatch at load time is fatal (ChecksumMismatch).
// On-disk format is little-endian by convention (we assume LE hosts and
// explicitly use to_le_bytes/from_le_bytes for portability if that changes).
//
// Versioning is two-tiered. At the FILE level, the superblock carries a
// packed MAJOR/MINOR `format_version` (I29); the open-time gate rejects
// a file whose MAJOR doesn't match the binary's. At the PAGE level, each
// non-superblock page carries a one-byte per-page format version (I31)
// that lets individual page layouts evolve within a MAJOR without a
// file-wide bump — the foundation for lazy per-page upgrade. See the
// PAGE_FORMAT_VERSION_CURRENT block below for the storage convention.
use xxh3_64;
// 8 KiB pages: small enough to keep per-page I/O cheap and the cache working
// set fine-grained, large enough to amortize header overhead. Changing this
// is a format break — FORMAT_VERSION must bump.
pub const PAGE_SIZE: usize = 8192;
pub const CHECKSUM_SIZE: usize = 8;
// Checksum lives at the very end of the page so that the entire header+body
// region (bytes 0..CHECKSUM_OFFSET) is a single contiguous hashable slice.
pub const CHECKSUM_OFFSET: usize = PAGE_SIZE - CHECKSUM_SIZE; // 8184
// Common page header (first 16 bytes) is shared by non-superblock pages: bytes
// 0..8 are page-type-specific (type tag at byte 0, per-page version at byte 1 —
// or byte 2 for HandleTable — plus any type header fields) and bytes 8..16 are
// the I31 reserved common-header region (COMMON_RESERVED_OFFSET..+LEN).
// PageCache identifies a page's type from byte 0 without knowing the concrete
// module. The superblock uses its own layout and does NOT carry this header (it
// stores its txn_counter in bytes 8..16).
//
// `#[allow(dead_code)]`: documents the layout commitment that every page-type
// module's `init_page` agrees with — the single source of truth even though no
// current call site reads it. It MUST equal DATA_PAGE_HEADER_SIZE and
// COMMON_RESERVED_OFFSET + COMMON_RESERVED_LEN; the const-asserts below lock
// that. I133 (2026-06-21): this was 12 — a stale value predating the I31 8..16
// reservation that contradicted its own sibling constants and every comment;
// corrected to 16. A workflow confirmed no page type stores data in 8..16, so
// the reserved region is genuine and the common header genuinely runs to 16.
pub const COMMON_HEADER_SIZE: usize = 16;
// Data pages carry an extended 16-byte header (common header + slot-array
// metadata). PAGE_BODY_SIZE is the space available to slot payloads after
// subtracting that header and the trailing checksum.
pub const DATA_PAGE_HEADER_SIZE: usize = 16;
pub const PAGE_BODY_SIZE: usize = PAGE_SIZE - DATA_PAGE_HEADER_SIZE - CHECKSUM_SIZE; // 8168
// Per-page format version (ISSUES.md I31). Each non-superblock page
// carries a one-byte version that lets future binaries read older
// page layouts without a MAJOR-format bump — the basis for lazy
// per-page upgrade. Version 0 is reserved for "the layout as of the
// I31 commit" (byte values pre-this-change happen to be zero already,
// which is why introducing this byte does not require breaking
// existing files).
//
// Storage offset is per-type:
// - Data, Overflow, FreeMap: byte 1 (was unused / padding)
// - HandleTable: byte 2 (byte 1 holds FLAG_LEAF/INTERIOR)
//
// Bytes 8..16 of every non-superblock page are RESERVED for future
// common-header fields (64 bits of headroom). Today they are
// universally zero. Adding a field there in the future will bump
// the relevant page type's per-page version, not the superblock's
// MAJOR.
pub const PAGE_FORMAT_VERSION_CURRENT: u8 = 0;
// I31 (ISSUES.md): reserved-region constants describe the 8-byte
// common-header headroom every non-superblock page leaves for future
// fields. No live code reads them today; they exist so that whenever a
// new common field IS added (bumping the relevant per-page version),
// the offset comes from one authoritative spot, not a fresh literal.
pub const COMMON_RESERVED_OFFSET: usize = 8;
pub const COMMON_RESERVED_LEN: usize = 8;
// I133 (ISSUES.md, 2026-06-21): lock the header-size constants so the 12-vs-16
// drift cannot recur. The common header spans bytes 0..16 (0..8 type-specific,
// 8..16 reserved), so it must equal both the reserved region's end and the
// data-page header — whose slot directory begins at byte 16, leaving no fields
// in the reserved tail. The per-page version byte (1, or 2 for HandleTable)
// sits below the reserved window. These are compile-time checks: a future edit
// that desyncs the constants fails to build.
const _: = assert!;
const _: = assert!;
const _: = assert!;
// "CHSL" in ASCII, stored little-endian so it appears as C-H-S-L when you
// hexdump the first 4 bytes of the file. Used to reject non-Chisel files
// before we even look at the checksum.
pub const MAGIC: u32 = 0x4348534C; // "CHSL"
// On-disk format version: byte-packed u32 with upper 16 bits = MAJOR,
// lower 16 bits = MINOR. Gates at open time on MAJOR only; same-major
// files are read-compatible regardless of minor (additive-only layout
// changes within a major are the invariant that makes this safe).
// Write safety across minors is a separate concern — a binary at minor
// M opening a file at minor M' > M can read but not safely write
// without clobbering fields it doesn't know about; this check is
// deferred until the first 1.1 release (at which point the gate grows
// a "newer minor ⇒ refuse writes" arm). See ISSUES.md I29.
//
// Pre-1.0 files (format_version = 1 or 2 in the old flat scheme) have
// major byte = 0 and are rejected with UnsupportedFormatVersion — a
// clean break, since there are no production DBs yet.
pub const FORMAT_MAJOR_VERSION: u16 = 1;
pub const FORMAT_MINOR_VERSION: u16 = 1;
/// MAJOR version stamped into an ENCRYPTED database's superblock. The bump from
/// 1 → 2 hard-rejects old binaries (which gate on FORMAT_MAJOR_VERSION == 1).
pub const FORMAT_MAJOR_VERSION_ENCRYPTED: u16 = 2;
/// MINOR version for the encrypted-DB format series. A new MAJOR series starts
/// its MINOR count at 0, so encrypted DBs stamp (2, 0) — NOT (2, FORMAT_MINOR_VERSION).
/// The encrypted format carries its own minor series, independent of plaintext.
pub const FORMAT_MINOR_VERSION_ENCRYPTED: u16 = 0;
/// The SINGLE canonical packed format_version for an ENCRYPTED database's
/// superblock: `pack(2, 0)`. `format_version_encrypted()` returns this constant;
/// create, open, and the superblock-body AAD (`sb_identity_aad`) all route
/// through that function, so there is exactly ONE on-disk value — no two
/// constants that can drift. An encryption-unaware binary (FORMAT_MAJOR_VERSION
/// == 1) rejects it as `UnsupportedFormatVersion`, the intended hard-reject.
pub const ENCRYPTED_FORMAT_VERSION: u32 = pack_format_version;
/// The encrypted-DB packed format version (MAJOR=2, MINOR=0). Single source of
/// truth: returns `ENCRYPTED_FORMAT_VERSION`. Called by the create path
/// (keys.rs, superblock::new_empty_encrypted), the commit stamp, and the
/// open-time gate — they all agree because they all call this one function.
/// Pack a (major, minor) pair into the on-disk u32 format version.
pub const
/// Extract the major-version byte pair from a packed format_version.
pub const
/// Extract the minor-version pair from a packed `format_version`. Companion to
/// `format_major`; used by the open-time I29 write-gate — a binary whose
/// MINOR is below the file's must not write the file (it would drop fields it
/// doesn't know). See ISSUES.md I29.
pub const
pub const FORMAT_VERSION: u32 = pack_format_version;
/// Sentinel value meaning "not yet allocated" for root page pointers
/// (e.g. an empty database has no handle-table or freemap root yet).
/// u64::MAX is used because 0 is a legitimate page id.
pub const PAGE_ID_NONE: u64 = u64MAX;
/// On-disk page type tag, stored as a single byte in the common header.
/// Discriminants are explicit and stable — changing them is a format break.
/// 0x00 is intentionally reserved so a zeroed/uninitialized page cannot be
/// mistaken for a valid type.
/// The per-page format version a freshly-initialized page of `page_type`
/// stamps — the single source of truth for the write side (every `init_page`
/// site calls this). Returns 0 for every type today. When a page type's layout
/// gains a version-requiring field, ONLY that type's arm changes; others stay
/// put (per-type format evolution — see docs/specs/2026-06-21-per-page-format-versioning-design.md). The match is exhaustive on purpose — adding
/// a `PageType` variant forces a decision here rather than defaulting silently.
/// See ISSUES.md I31.
pub const
/// Read the per-page format version stamped in `buf`. Dispatches the byte
/// offset on the page-type tag at byte 0: HandleTable keeps byte 1 for its
/// leaf/interior flag and stores the version at byte 2; every other
/// non-superblock page stores it at byte 1. A reader that must distinguish
/// layouts (an additive field where zero is a legitimate value) branches on
/// this: `if page_format_version(buf) >= K { read field } else { default }`.
/// See ISSUES.md I31 and docs/specs/2026-06-21-per-page-format-versioning-design.md.
// `#[allow(dead_code)]`: forward-looking API seam — read-side version gates
// (I31 follow-on) are not yet wired in, so no call site exists today.
// XXH3 was chosen over CRC32C for throughput on modern CPUs. It is a
// non-cryptographic hash — sufficient for detecting disk corruption, NOT
// a defense against adversarial tampering.
/// Compute the XXH3 checksum for a page buffer (over bytes 0..CHECKSUM_OFFSET).
/// The checksum region itself is excluded so that stamp/verify are symmetric.
/// Write the checksum into the last 8 bytes of the page buffer.
/// Must be called after every mutation and before the page is handed to
/// page_io for writing — otherwise the next read will see a stale checksum
/// and treat the page as corrupt.
/// Verify the checksum in the last 8 bytes matches the computed checksum.
/// PageCache calls this on every read; callers should not need to invoke it
/// directly. A `false` result must be reported as ChecksumMismatch (fatal).