Skip to main content

fsqlite_types/
lib.rs

1pub mod cx;
2pub mod ecs;
3pub mod encoding;
4pub mod eprocess;
5pub mod flags;
6pub mod glossary;
7pub mod limits;
8pub mod obligation;
9pub mod opcode;
10pub mod record;
11pub mod serial_type;
12pub mod sync_primitives;
13pub mod value;
14
15pub use cx::Cx;
16pub use ecs::{
17    ObjectId, PayloadHash, SYMBOL_RECORD_MAGIC, SYMBOL_RECORD_VERSION, SymbolReadPath,
18    SymbolRecord, SymbolRecordError, SymbolRecordFlags, SystematicLayoutError,
19    layout_systematic_run, reconstruct_systematic_happy_path, recover_object_with_fallback,
20    source_symbol_count, validate_systematic_run,
21};
22pub use eprocess::{
23    EProcessConfig, EProcessDecision, EProcessOracle, EProcessSignal, EProcessSnapshot,
24    EProcessTelemetryBridge,
25};
26pub use glossary::{
27    ArcCache, BtreeRef, Budget, COMMIT_MARKER_RECORD_V1_SIZE, ColumnIdx, CommitCapsule,
28    CommitMarker, CommitProof, CommitSeq, DecodeProof, DependencyEdge, EpochId, IdempotencyKey,
29    IndexId, IntentFootprint, IntentLog, IntentOp, IntentOpKind, OTI_WIRE_SIZE, OperatingMode, Oti,
30    Outcome, PageHistory, PageVersion, RangeKey, ReadWitness, RebaseBinaryOp, RebaseExpr,
31    RebaseUnaryOp, Region, RemoteCap, RootManifest, RowId, RowIdAllocator, RowIdExhausted,
32    RowIdMode, Saga, SchemaEpoch, SemanticKeyKind, SemanticKeyRef, Snapshot, StructuralEffects,
33    SymbolAuthMasterKeyCap, SymbolValidityWindow, TableId, TxnEpoch, TxnId, TxnSlot, TxnToken,
34    VersionPointer, WitnessIndexSegment, WitnessKey, WriteWitness,
35};
36pub use value::SqliteValue;
37
38use std::fmt;
39use std::num::NonZeroU32;
40
41/// A page number in the database file.
42///
43/// Page numbers are 1-based (page 0 does not exist). Page 1 is the database
44/// header page. The maximum page count is `u32::MAX - 1` (4,294,967,294).
45#[derive(
46    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
47)]
48#[repr(transparent)]
49pub struct PageNumber(NonZeroU32);
50
51impl PageNumber {
52    /// Page 1 is the database header page containing the file header and the
53    /// schema table root.
54    pub const ONE: Self = Self(NonZeroU32::MIN);
55
56    /// Create a new page number from a raw u32.
57    ///
58    /// Returns `None` if `n` is 0 (page 0 does not exist in SQLite).
59    #[inline]
60    pub const fn new(n: u32) -> Option<Self> {
61        match NonZeroU32::new(n) {
62            Some(v) => Some(Self(v)),
63            None => None,
64        }
65    }
66
67    /// Get the raw u32 value.
68    #[inline]
69    pub const fn get(self) -> u32 {
70        self.0.get()
71    }
72}
73
74impl fmt::Display for PageNumber {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "{}", self.0)
77    }
78}
79
80impl TryFrom<u32> for PageNumber {
81    type Error = InvalidPageNumber;
82
83    fn try_from(value: u32) -> Result<Self, Self::Error> {
84        Self::new(value).ok_or(InvalidPageNumber)
85    }
86}
87
88/// Fast identity hasher for `PageNumber` keys in lock/commit tables.
89///
90/// Page numbers are already well-distributed u32 values, so we skip
91/// hashing entirely and use the raw value directly.
92#[derive(Default)]
93pub struct PageNumberHasher(u64);
94
95impl std::hash::Hasher for PageNumberHasher {
96    fn write(&mut self, _: &[u8]) {
97        // PageNumber's Hash impl calls write_u32 (via NonZeroU32). If this
98        // method is reached, the hasher is being misused with a non-u32 key.
99        debug_assert!(false, "PageNumberHasher only supports write_u32");
100    }
101
102    fn write_u32(&mut self, n: u32) {
103        self.0 = u64::from(n);
104    }
105
106    fn finish(&self) -> u64 {
107        self.0
108    }
109}
110
111/// BuildHasher for `PageNumberHasher`.
112pub type PageNumberBuildHasher = std::hash::BuildHasherDefault<PageNumberHasher>;
113
114/// GF(256) addition (`+`) for bytes (XOR).
115#[must_use]
116pub const fn gf256_add_byte(lhs: u8, rhs: u8) -> u8 {
117    lhs ^ rhs
118}
119
120/// Scalar GF(256) multiply with irreducible polynomial `0x11d`.
121///
122/// This is the core algebraic primitive used for RaptorQ encoding and
123/// XOR-delta compression (§3.2.1).
124#[must_use]
125pub fn gf256_mul_byte(mut a: u8, mut b: u8) -> u8 {
126    let mut out = 0_u8;
127    while b != 0 {
128        if (b & 1) != 0 {
129            out ^= a;
130        }
131        let carry = (a & 0x80) != 0;
132        a <<= 1;
133        if carry {
134            a ^= 0x1D;
135        }
136        b >>= 1;
137    }
138    out
139}
140
141/// Multiplicative inverse in GF(256) (`None` for zero).
142#[must_use]
143pub fn gf256_inverse_byte(value: u8) -> Option<u8> {
144    if value == 0 {
145        return None;
146    }
147    for candidate in 1u16..=255 {
148        let inv = u8::try_from(candidate).expect("candidate in 1..=255 always fits u8");
149        if gf256_mul_byte(value, inv) == 1 {
150            return Some(inv);
151        }
152    }
153    None
154}
155
156/// SQLite page categories relevant to merge safety policy.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
158pub enum MergePageKind {
159    /// Interior table b-tree page (0x05).
160    BtreeInteriorTable,
161    /// Leaf table b-tree page (0x0D).
162    BtreeLeafTable,
163    /// Interior index b-tree page (0x02).
164    BtreeInteriorIndex,
165    /// Leaf index b-tree page (0x0A).
166    BtreeLeafIndex,
167    /// Overflow page.
168    Overflow,
169    /// Freelist trunk/leaf page.
170    Freelist,
171    /// Pointer-map page.
172    PointerMap,
173    /// Opaque/non-SQLite-structured page.
174    Opaque,
175}
176
177impl MergePageKind {
178    /// Whether this page has SQLite-internal pointer semantics.
179    #[must_use]
180    pub const fn is_sqlite_structured(self) -> bool {
181        !matches!(self, Self::Opaque)
182    }
183
184    /// Classify a raw page image for merge-safety policy checks.
185    #[must_use]
186    pub fn classify(page: &[u8]) -> Self {
187        let Some(first_byte) = page.first().copied() else {
188            return Self::Opaque;
189        };
190        match BTreePageType::from_byte(first_byte) {
191            Some(BTreePageType::LeafTable) => Self::BtreeLeafTable,
192            Some(BTreePageType::InteriorTable) => Self::BtreeInteriorTable,
193            Some(BTreePageType::LeafIndex) => Self::BtreeLeafIndex,
194            Some(BTreePageType::InteriorIndex) => Self::BtreeInteriorIndex,
195            None => Self::Opaque,
196        }
197    }
198}
199
200/// Error returned when attempting to create a `PageNumber` from 0.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct InvalidPageNumber;
203
204impl fmt::Display for InvalidPageNumber {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.write_str("page number cannot be zero")
207    }
208}
209
210impl std::error::Error for InvalidPageNumber {}
211
212/// Database page size in bytes.
213///
214/// Must be a power of two between 512 and 65536 (inclusive). The default is
215/// 4096 bytes, matching SQLite's `SQLITE_DEFAULT_PAGE_SIZE`.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
217pub struct PageSize(u32);
218
219impl PageSize {
220    /// Minimum page size: 512 bytes.
221    pub const MIN: Self = Self(512);
222
223    /// Default page size: 4096 bytes.
224    pub const DEFAULT: Self = Self(limits::DEFAULT_PAGE_SIZE);
225
226    /// Maximum page size: 65536 bytes.
227    pub const MAX: Self = Self(limits::MAX_PAGE_SIZE);
228
229    /// Create a new page size, validating that it is a power of two in
230    /// the range \[512, 65536\].
231    pub const fn new(size: u32) -> Option<Self> {
232        if size < 512 || size > 65536 || !size.is_power_of_two() {
233            None
234        } else {
235            Some(Self(size))
236        }
237    }
238
239    /// Get the raw page size in bytes.
240    #[inline]
241    pub const fn get(self) -> u32 {
242        self.0
243    }
244
245    /// Get the page size as a `usize`.
246    #[inline]
247    pub const fn as_usize(self) -> usize {
248        self.0 as usize
249    }
250
251    /// The usable size of a page (total size minus reserved bytes at the end).
252    ///
253    /// `reserved` is the number of bytes reserved at the end of each page
254    /// for extensions (typically 0, stored at byte offset 20 of the header).
255    #[inline]
256    pub const fn usable(self, reserved: u8) -> u32 {
257        self.0 - reserved as u32
258    }
259}
260
261impl Default for PageSize {
262    fn default() -> Self {
263        Self::DEFAULT
264    }
265}
266
267impl fmt::Display for PageSize {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        write!(f, "{}", self.0)
270    }
271}
272
273/// Raw page data as an owned byte buffer.
274///
275/// The length always matches the database page size.
276/// Uses `Arc` for cheap cloning (Copy-On-Write).
277#[derive(Clone, PartialEq, Eq)]
278pub struct PageData {
279    data: std::sync::Arc<Vec<u8>>,
280}
281
282impl PageData {
283    /// Create a zero-filled page of the given size.
284    pub fn zeroed(size: PageSize) -> Self {
285        Self {
286            data: std::sync::Arc::new(vec![0u8; size.as_usize()]),
287        }
288    }
289
290    /// Create from existing bytes. The caller must ensure the length matches
291    /// the page size.
292    pub fn from_vec(data: Vec<u8>) -> Self {
293        Self {
294            data: std::sync::Arc::new(data),
295        }
296    }
297
298    /// Get the page data as a byte slice.
299    #[inline]
300    pub fn as_bytes(&self) -> &[u8] {
301        &self.data
302    }
303
304    /// Get the page data as a mutable byte slice.
305    ///
306    /// This performs a clone if the data is shared (Copy-On-Write).
307    #[inline]
308    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
309        std::sync::Arc::make_mut(&mut self.data).as_mut_slice()
310    }
311
312    /// Get the length in bytes.
313    #[inline]
314    pub fn len(&self) -> usize {
315        self.data.len()
316    }
317
318    /// Returns true if the page data is empty (should never be true for valid pages).
319    #[inline]
320    pub fn is_empty(&self) -> bool {
321        self.data.is_empty()
322    }
323
324    /// Consume self and return the inner `Vec<u8>`.
325    ///
326    /// If the data is shared, this clones the vector.
327    pub fn into_vec(self) -> Vec<u8> {
328        match std::sync::Arc::try_unwrap(self.data) {
329            Ok(v) => v,
330            Err(arc) => (*arc).clone(),
331        }
332    }
333}
334
335impl fmt::Debug for PageData {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        f.debug_struct("PageData")
338            .field("len", &self.data.len())
339            .finish()
340    }
341}
342
343impl AsRef<[u8]> for PageData {
344    fn as_ref(&self) -> &[u8] {
345        &self.data
346    }
347}
348
349impl AsMut<[u8]> for PageData {
350    fn as_mut(&mut self) -> &mut [u8] {
351        std::sync::Arc::make_mut(&mut self.data).as_mut_slice()
352    }
353}
354
355/// SQLite type affinity, used for column type resolution.
356///
357/// See <https://www.sqlite.org/datatype3.html#type_affinity>.
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
359#[repr(u8)]
360pub enum TypeAffinity {
361    /// Column prefers integer storage. Includes INTEGER, INT, TINYINT, etc.
362    Integer = b'D',
363    /// Column prefers text storage. Includes TEXT, VARCHAR, CLOB.
364    Text = b'B',
365    /// Column has no preference. Includes BLOB or no type specified.
366    Blob = b'A',
367    /// Column prefers real (float) storage. Includes REAL, DOUBLE, FLOAT.
368    Real = b'E',
369    /// Column prefers numeric storage. Includes NUMERIC, DECIMAL, BOOLEAN,
370    /// DATE, DATETIME.
371    Numeric = b'C',
372}
373
374impl TypeAffinity {
375    /// Determine the type affinity for a declared column type name.
376    ///
377    /// Uses SQLite's first-match rule (§3.1 of datatype3.html):
378    /// 1. Contains "INT" → INTEGER
379    /// 2. Contains "CHAR", "CLOB", or "TEXT" → TEXT
380    /// 3. Contains "BLOB" or is empty → BLOB
381    /// 4. Contains "REAL", "FLOA", or "DOUB" → REAL
382    /// 5. Otherwise → NUMERIC
383    pub fn from_type_name(type_name: &str) -> Self {
384        let upper = type_name.to_ascii_uppercase();
385
386        if upper.contains("INT") {
387            Self::Integer
388        } else if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
389            Self::Text
390        } else if upper.is_empty() || upper.contains("BLOB") {
391            Self::Blob
392        } else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
393            Self::Real
394        } else {
395            Self::Numeric
396        }
397    }
398
399    /// Determine the affinity to apply for a comparison between two operands.
400    ///
401    /// Returns `Some(affinity)` if one side needs coercion, `None` if no
402    /// coercion is needed. The returned affinity should be applied to the
403    /// operand that needs conversion.
404    ///
405    /// Rules (§3.2 of datatype3.html):
406    /// - If one operand is INTEGER/REAL/NUMERIC and the other is TEXT/BLOB,
407    ///   apply numeric affinity to the TEXT/BLOB side.
408    /// - If one operand is TEXT and the other is BLOB (no numeric involved),
409    ///   apply TEXT affinity to the BLOB side.
410    /// - Same affinity or both BLOB → no coercion.
411    pub fn comparison_affinity(left: Self, right: Self) -> Option<Self> {
412        if left == right {
413            return None;
414        }
415
416        let is_numeric = |a: Self| matches!(a, Self::Integer | Self::Real | Self::Numeric);
417
418        // Rule 1: numeric vs TEXT/BLOB → apply numeric affinity
419        if is_numeric(left) && matches!(right, Self::Text | Self::Blob) {
420            return Some(Self::Numeric);
421        }
422        if is_numeric(right) && matches!(left, Self::Text | Self::Blob) {
423            return Some(Self::Numeric);
424        }
425
426        // Rule 2: TEXT vs BLOB → apply TEXT affinity to BLOB side
427        if (left == Self::Text && right == Self::Blob)
428            || (left == Self::Blob && right == Self::Text)
429        {
430            return Some(Self::Text);
431        }
432
433        // Rule 3: no coercion
434        None
435    }
436}
437
438/// The five fundamental SQLite storage classes.
439///
440/// Every value stored in SQLite belongs to exactly one of these classes.
441/// See <https://www.sqlite.org/datatype3.html>.
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
443#[repr(u8)]
444pub enum StorageClass {
445    /// SQL NULL.
446    Null = 1,
447    /// A signed 64-bit integer.
448    Integer = 2,
449    /// An IEEE 754 64-bit float.
450    Real = 3,
451    /// A UTF-8 text string.
452    Text = 4,
453    /// A binary large object.
454    Blob = 5,
455}
456
457impl fmt::Display for StorageClass {
458    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459        match self {
460            Self::Null => f.write_str("NULL"),
461            Self::Integer => f.write_str("INTEGER"),
462            Self::Real => f.write_str("REAL"),
463            Self::Text => f.write_str("TEXT"),
464            Self::Blob => f.write_str("BLOB"),
465        }
466    }
467}
468
469/// Column types valid in STRICT tables.
470///
471/// STRICT tables enforce that every non-NULL value stored in a column matches
472/// the declared type. See <https://www.sqlite.org/stricttables.html>.
473#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
474pub enum StrictColumnType {
475    /// Only INTEGER storage class (and NULL).
476    Integer,
477    /// REAL storage class; integers are implicitly converted to REAL (and NULL).
478    Real,
479    /// Only TEXT storage class (and NULL).
480    Text,
481    /// Only BLOB storage class (and NULL).
482    Blob,
483    /// Any storage class accepted without coercion.
484    Any,
485}
486
487impl StrictColumnType {
488    /// Parse a STRICT column type from a type name string.
489    ///
490    /// Returns `None` if the type name is not a valid STRICT type.
491    /// Valid STRICT types: INT, INTEGER, REAL, TEXT, BLOB, ANY.
492    pub fn from_type_name(name: &str) -> Option<Self> {
493        match name.to_ascii_uppercase().as_str() {
494            "INT" | "INTEGER" => Some(Self::Integer),
495            "REAL" => Some(Self::Real),
496            "TEXT" => Some(Self::Text),
497            "BLOB" => Some(Self::Blob),
498            "ANY" => Some(Self::Any),
499            _ => None,
500        }
501    }
502}
503
504/// Error returned when a value violates a STRICT table column type constraint.
505#[derive(Debug, Clone, PartialEq, Eq)]
506pub struct StrictTypeError {
507    /// The expected strict column type.
508    pub expected: StrictColumnType,
509    /// The actual storage class of the value.
510    pub actual: StorageClass,
511}
512
513impl fmt::Display for StrictTypeError {
514    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515        write!(
516            f,
517            "cannot store {} value in {:?} column",
518            self.actual, self.expected
519        )
520    }
521}
522
523/// Encoding used for text in the database.
524#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
525#[repr(u8)]
526pub enum TextEncoding {
527    /// UTF-8 encoding (the most common).
528    #[default]
529    Utf8 = 1,
530    /// UTF-16le (little-endian).
531    Utf16le = 2,
532    /// UTF-16be (big-endian).
533    Utf16be = 3,
534}
535
536/// Journal mode for the database connection.
537#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
538pub enum JournalMode {
539    /// Delete the rollback journal after each transaction.
540    #[default]
541    Delete,
542    /// Truncate the rollback journal to zero length.
543    Truncate,
544    /// Persist the rollback journal (don't delete, just zero the header).
545    Persist,
546    /// Store rollback journal in memory only.
547    Memory,
548    /// Write-ahead logging.
549    Wal,
550    /// Completely disable the rollback journal.
551    Off,
552}
553
554/// Synchronous mode for database writes.
555#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
556#[repr(u8)]
557pub enum SynchronousMode {
558    /// No syncs at all. Maximum speed, minimum safety.
559    Off = 0,
560    /// Sync at critical moments. Good balance.
561    Normal = 1,
562    /// Sync after each write. Maximum safety.
563    #[default]
564    Full = 2,
565    /// Like Full, but also sync the directory after creating files.
566    Extra = 3,
567}
568
569/// Lock level for database file locking (SQLite's five-state lock).
570#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
571#[repr(u8)]
572pub enum LockLevel {
573    /// No lock held.
574    #[default]
575    None = 0,
576    /// Shared lock (reading).
577    Shared = 1,
578    /// Reserved lock (intending to write).
579    Reserved = 2,
580    /// Pending lock (waiting for shared locks to clear).
581    Pending = 3,
582    /// Exclusive lock (writing).
583    Exclusive = 4,
584}
585
586/// WAL checkpoint mode.
587#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
588#[repr(u8)]
589pub enum CheckpointMode {
590    /// Checkpoint as many frames as possible without waiting.
591    Passive = 0,
592    /// Block until all frames are checkpointed.
593    Full = 1,
594    /// Like Full, then truncate the WAL file.
595    Restart = 2,
596    /// Like Restart, then truncate WAL to zero bytes.
597    Truncate = 3,
598}
599
600/// The 100-byte database file header layout.
601///
602/// This struct represents the parsed content of the first 100 bytes of a
603/// SQLite database file.
604#[derive(Debug, Clone, PartialEq, Eq)]
605pub struct DatabaseHeader {
606    /// Page size in bytes (stored as big-endian u16 at offset 16; value 1 means 65536).
607    pub page_size: PageSize,
608    /// File format write version (1 = legacy, 2 = WAL).
609    pub write_version: u8,
610    /// File format read version (1 = legacy, 2 = WAL).
611    pub read_version: u8,
612    /// Reserved bytes per page (at offset 20).
613    pub reserved_per_page: u8,
614    /// File change counter (at offset 24).
615    pub change_counter: u32,
616    /// Total number of pages in the database file.
617    pub page_count: u32,
618    /// Page number of the first freelist trunk page (0 if none).
619    pub freelist_trunk: u32,
620    /// Total number of freelist pages.
621    pub freelist_count: u32,
622    /// Schema cookie (incremented on schema changes).
623    pub schema_cookie: u32,
624    /// Schema format number (currently 4).
625    pub schema_format: u32,
626    /// Default page cache size (from `PRAGMA default_cache_size`).
627    pub default_cache_size: i32,
628    /// Largest root page number for auto-vacuum/incremental-vacuum (0 if not auto-vacuum).
629    pub largest_root_page: u32,
630    /// Database text encoding (1=UTF8, 2=UTF16le, 3=UTF16be).
631    pub text_encoding: TextEncoding,
632    /// User version (from `PRAGMA user_version`).
633    pub user_version: u32,
634    /// Non-zero for incremental vacuum mode.
635    pub incremental_vacuum: u32,
636    /// Application ID (from `PRAGMA application_id`).
637    pub application_id: u32,
638    /// Version-valid-for number (the change counter value when the version
639    /// number was stored).
640    pub version_valid_for: u32,
641    /// SQLite version number that created the database.
642    pub sqlite_version: u32,
643}
644
645impl Default for DatabaseHeader {
646    fn default() -> Self {
647        Self {
648            page_size: PageSize::DEFAULT,
649            write_version: 1,
650            read_version: 1,
651            reserved_per_page: 0,
652            change_counter: 0,
653            page_count: 0,
654            freelist_trunk: 0,
655            freelist_count: 0,
656            schema_cookie: 0,
657            schema_format: 4,
658            default_cache_size: -2000,
659            largest_root_page: 0,
660            text_encoding: TextEncoding::Utf8,
661            user_version: 0,
662            incremental_vacuum: 0,
663            application_id: 0,
664            version_valid_for: 0,
665            sqlite_version: 0,
666        }
667    }
668}
669
670/// The magic string at the start of every SQLite database file.
671pub const DATABASE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
672
673/// Size of the database file header in bytes.
674pub const DATABASE_HEADER_SIZE: usize = 100;
675
676/// Maximum SQLite file format version supported by this codebase.
677///
678/// This corresponds to WAL support (`2`). If the database header's read version exceeds this
679/// value, the database must be refused. If only the write version exceeds this value, the
680/// database may be opened read-only.
681pub const MAX_FILE_FORMAT_VERSION: u8 = 2;
682
683/// SQLite version number written into the database header for FrankenSQLite-created databases.
684///
685/// This matches SQLite 3.52.0 (`3052000`), which is the conformance target for this project.
686pub const FRANKENSQLITE_SQLITE_VERSION_NUMBER: u32 = 3_052_000;
687
688/// Database file open mode derived from the header's read/write version bytes.
689#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
690pub enum DatabaseOpenMode {
691    /// The database can be opened read-write.
692    ReadWrite,
693    /// The database can only be opened read-only (write version too new).
694    ReadOnly,
695}
696
697/// Errors that can occur while parsing or validating the 100-byte database header.
698#[derive(Debug, Clone, PartialEq, Eq)]
699pub enum DatabaseHeaderError {
700    /// Magic string mismatch at bytes 0..16.
701    InvalidMagic,
702    /// Page size encoding was invalid.
703    InvalidPageSize { raw: u16 },
704    /// Embedded payload fractions (bytes 21..24) are invalid.
705    InvalidPayloadFractions { max: u8, min: u8, leaf: u8 },
706    /// The effective usable page size would be below the minimum allowed by SQLite (480).
707    UsableSizeTooSmall {
708        page_size: u32,
709        reserved_per_page: u8,
710        usable_size: u32,
711    },
712    /// Read file format version is too new to be understood.
713    UnsupportedReadVersion { read_version: u8, max_supported: u8 },
714    /// Text encoding field was not 1/2/3.
715    InvalidTextEncoding { raw: u32 },
716    /// Schema format number is unsupported.
717    InvalidSchemaFormat { raw: u32 },
718}
719
720impl fmt::Display for DatabaseHeaderError {
721    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
722        match self {
723            Self::InvalidMagic => f.write_str("invalid database header magic"),
724            Self::InvalidPageSize { raw } => write!(f, "invalid page size encoding: {raw}"),
725            Self::InvalidPayloadFractions { max, min, leaf } => write!(
726                f,
727                "invalid payload fractions: max={max} min={min} leaf={leaf}"
728            ),
729            Self::UsableSizeTooSmall {
730                page_size,
731                reserved_per_page,
732                usable_size,
733            } => write!(
734                f,
735                "usable page size too small: page_size={page_size} reserved={reserved_per_page} usable={usable_size}"
736            ),
737            Self::UnsupportedReadVersion {
738                read_version,
739                max_supported,
740            } => write!(
741                f,
742                "unsupported read format version: read_version={read_version} max_supported={max_supported}"
743            ),
744            Self::InvalidTextEncoding { raw } => write!(f, "invalid text encoding: {raw}"),
745            Self::InvalidSchemaFormat { raw } => write!(f, "invalid schema format: {raw}"),
746        }
747    }
748}
749
750impl std::error::Error for DatabaseHeaderError {}
751
752impl DatabaseHeader {
753    /// Parse and validate a 100-byte database header.
754    pub fn from_bytes(buf: &[u8; DATABASE_HEADER_SIZE]) -> Result<Self, DatabaseHeaderError> {
755        if &buf[..DATABASE_HEADER_MAGIC.len()] != DATABASE_HEADER_MAGIC {
756            return Err(DatabaseHeaderError::InvalidMagic);
757        }
758
759        let page_size_raw = encoding::read_u16_be(&buf[16..18]).expect("fixed u16 field");
760        let page_size_u32 = match page_size_raw {
761            1 => 65_536,
762            0 => return Err(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw }),
763            n => u32::from(n),
764        };
765        let page_size = PageSize::new(page_size_u32)
766            .ok_or(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw })?;
767
768        let write_version = buf[18];
769        let read_version = buf[19];
770        let reserved_per_page = buf[20];
771
772        let max_payload = buf[21];
773        let min_payload = buf[22];
774        let leaf_payload = buf[23];
775        if (max_payload, min_payload, leaf_payload) != (64, 32, 32) {
776            return Err(DatabaseHeaderError::InvalidPayloadFractions {
777                max: max_payload,
778                min: min_payload,
779                leaf: leaf_payload,
780            });
781        }
782
783        let usable_size = page_size.usable(reserved_per_page);
784        if usable_size < 480 {
785            return Err(DatabaseHeaderError::UsableSizeTooSmall {
786                page_size: page_size.get(),
787                reserved_per_page,
788                usable_size,
789            });
790        }
791
792        // Read version governs forward compatibility: refuse if too new.
793        if read_version > MAX_FILE_FORMAT_VERSION {
794            return Err(DatabaseHeaderError::UnsupportedReadVersion {
795                read_version,
796                max_supported: MAX_FILE_FORMAT_VERSION,
797            });
798        }
799
800        let change_counter = encoding::read_u32_be(&buf[24..28]).expect("fixed u32 field");
801        let page_count = encoding::read_u32_be(&buf[28..32]).expect("fixed u32 field");
802        let freelist_trunk = encoding::read_u32_be(&buf[32..36]).expect("fixed u32 field");
803        let freelist_count = encoding::read_u32_be(&buf[36..40]).expect("fixed u32 field");
804        let schema_cookie = encoding::read_u32_be(&buf[40..44]).expect("fixed u32 field");
805        let schema_format = encoding::read_u32_be(&buf[44..48]).expect("fixed u32 field");
806
807        // This project intentionally does not support legacy schema formats.
808        // See README: "What We Deliberately Exclude".
809        if schema_format != 4 {
810            return Err(DatabaseHeaderError::InvalidSchemaFormat { raw: schema_format });
811        }
812
813        let default_cache_size = encoding::read_i32_be(&buf[48..52]).expect("fixed i32 field");
814        let largest_root_page = encoding::read_u32_be(&buf[52..56]).expect("fixed u32 field");
815
816        let text_encoding_raw = encoding::read_u32_be(&buf[56..60]).expect("fixed u32 field");
817        let text_encoding = match text_encoding_raw {
818            1 => TextEncoding::Utf8,
819            2 => TextEncoding::Utf16le,
820            3 => TextEncoding::Utf16be,
821            _ => {
822                return Err(DatabaseHeaderError::InvalidTextEncoding {
823                    raw: text_encoding_raw,
824                });
825            }
826        };
827
828        let user_version = encoding::read_u32_be(&buf[60..64]).expect("fixed u32 field");
829        let incremental_vacuum = encoding::read_u32_be(&buf[64..68]).expect("fixed u32 field");
830        let application_id = encoding::read_u32_be(&buf[68..72]).expect("fixed u32 field");
831        let version_valid_for = encoding::read_u32_be(&buf[92..96]).expect("fixed u32 field");
832        let sqlite_version = encoding::read_u32_be(&buf[96..100]).expect("fixed u32 field");
833
834        Ok(Self {
835            page_size,
836            write_version,
837            read_version,
838            reserved_per_page,
839            change_counter,
840            page_count,
841            freelist_trunk,
842            freelist_count,
843            schema_cookie,
844            schema_format,
845            default_cache_size,
846            largest_root_page,
847            text_encoding,
848            user_version,
849            incremental_vacuum,
850            application_id,
851            version_valid_for,
852            sqlite_version,
853        })
854    }
855
856    /// Compute the open mode implied by the header's read/write version bytes.
857    pub const fn open_mode(
858        &self,
859        max_supported: u8,
860    ) -> Result<DatabaseOpenMode, DatabaseHeaderError> {
861        if self.read_version > max_supported {
862            return Err(DatabaseHeaderError::UnsupportedReadVersion {
863                read_version: self.read_version,
864                max_supported,
865            });
866        }
867        if self.write_version > max_supported {
868            return Ok(DatabaseOpenMode::ReadOnly);
869        }
870        Ok(DatabaseOpenMode::ReadWrite)
871    }
872
873    /// Check whether the header-derived database size might be stale.
874    ///
875    /// When `version_valid_for != change_counter`, header-derived fields
876    /// like `page_count` may be stale and should be recomputed from the
877    /// actual file size. This protects against partial header writes or
878    /// external modification.
879    pub const fn is_page_count_stale(&self) -> bool {
880        self.version_valid_for != self.change_counter
881    }
882
883    /// Compute the page count from the actual file size.
884    ///
885    /// This should be used when `is_page_count_stale()` returns true.
886    /// Returns `None` if the file size is not a multiple of the page size
887    /// or would exceed `u32::MAX` pages.
888    #[allow(clippy::cast_possible_truncation)]
889    pub const fn page_count_from_file_size(&self, file_size: u64) -> Option<u32> {
890        let ps = self.page_size.get() as u64;
891        if file_size == 0 || file_size % ps != 0 {
892            return None;
893        }
894        let count = file_size / ps;
895        if count > u32::MAX as u64 {
896            return None;
897        }
898        Some(count as u32)
899    }
900
901    /// Serialize this header into a 100-byte buffer.
902    pub fn write_to_bytes(
903        &self,
904        out: &mut [u8; DATABASE_HEADER_SIZE],
905    ) -> Result<(), DatabaseHeaderError> {
906        // Validate invariants we rely on for interoperability.
907        if self.schema_format != 4 {
908            return Err(DatabaseHeaderError::InvalidSchemaFormat {
909                raw: self.schema_format,
910            });
911        }
912
913        let usable_size = self.page_size.usable(self.reserved_per_page);
914        if usable_size < 480 {
915            return Err(DatabaseHeaderError::UsableSizeTooSmall {
916                page_size: self.page_size.get(),
917                reserved_per_page: self.reserved_per_page,
918                usable_size,
919            });
920        }
921
922        out.fill(0);
923        out[..DATABASE_HEADER_MAGIC.len()].copy_from_slice(DATABASE_HEADER_MAGIC);
924
925        // Page size (big-endian u16) where 1 encodes 65536.
926        let page_size_raw = if self.page_size.get() == 65_536 {
927            1u16
928        } else {
929            #[allow(clippy::cast_possible_truncation)]
930            {
931                self.page_size.get() as u16
932            }
933        };
934        encoding::write_u16_be(&mut out[16..18], page_size_raw).expect("fixed u16 field");
935
936        out[18] = self.write_version;
937        out[19] = self.read_version;
938        out[20] = self.reserved_per_page;
939
940        // Payload fractions must be 64/32/32.
941        out[21] = 64;
942        out[22] = 32;
943        out[23] = 32;
944
945        encoding::write_u32_be(&mut out[24..28], self.change_counter).expect("fixed u32 field");
946        encoding::write_u32_be(&mut out[28..32], self.page_count).expect("fixed u32 field");
947        encoding::write_u32_be(&mut out[32..36], self.freelist_trunk).expect("fixed u32 field");
948        encoding::write_u32_be(&mut out[36..40], self.freelist_count).expect("fixed u32 field");
949        encoding::write_u32_be(&mut out[40..44], self.schema_cookie).expect("fixed u32 field");
950        encoding::write_u32_be(&mut out[44..48], self.schema_format).expect("fixed u32 field");
951        encoding::write_i32_be(&mut out[48..52], self.default_cache_size).expect("fixed i32 field");
952        encoding::write_u32_be(&mut out[52..56], self.largest_root_page).expect("fixed u32 field");
953
954        let text_encoding_u32 = match self.text_encoding {
955            TextEncoding::Utf8 => 1u32,
956            TextEncoding::Utf16le => 2u32,
957            TextEncoding::Utf16be => 3u32,
958        };
959        encoding::write_u32_be(&mut out[56..60], text_encoding_u32).expect("fixed u32 field");
960
961        encoding::write_u32_be(&mut out[60..64], self.user_version).expect("fixed u32 field");
962        encoding::write_u32_be(&mut out[64..68], self.incremental_vacuum).expect("fixed u32 field");
963        encoding::write_u32_be(&mut out[68..72], self.application_id).expect("fixed u32 field");
964
965        // Bytes 72..92 are reserved for future expansion. We always write zeros.
966        encoding::write_u32_be(&mut out[92..96], self.version_valid_for).expect("fixed u32 field");
967        encoding::write_u32_be(&mut out[96..100], self.sqlite_version).expect("fixed u32 field");
968
969        Ok(())
970    }
971
972    /// Serialize this header to bytes.
973    pub fn to_bytes(&self) -> Result<[u8; DATABASE_HEADER_SIZE], DatabaseHeaderError> {
974        let mut out = [0u8; DATABASE_HEADER_SIZE];
975        self.write_to_bytes(&mut out)?;
976        Ok(out)
977    }
978}
979
980/// Maximum number of fragmented free bytes allowed on a B-tree page header.
981pub const BTREE_MAX_FRAGMENTED_FREE_BYTES: u8 = 60;
982
983/// Errors that can occur while parsing B-tree page layout structures.
984#[derive(Debug, Clone, PartialEq, Eq)]
985pub enum BTreePageError {
986    /// Page buffer did not match the expected page size.
987    PageSizeMismatch { expected: usize, actual: usize },
988    /// Page did not have enough bytes to read the header.
989    PageTooSmall { usable_size: usize, needed: usize },
990    /// Unknown B-tree page type byte.
991    InvalidPageType { raw: u8 },
992    /// Fragmented free bytes exceeds the maximum allowed.
993    InvalidFragmentedFreeBytes { raw: u8, max: u8 },
994    /// Cell content area start offset was invalid for this page.
995    InvalidCellContentAreaStart {
996        raw: u16,
997        decoded: u32,
998        usable_size: usize,
999    },
1000    /// Cell content area begins before the end of the cell pointer array.
1001    CellContentAreaOverlapsCellPointers {
1002        cell_content_start: u32,
1003        cell_pointer_array_end: usize,
1004    },
1005    /// Cell pointer array extends past the usable page area.
1006    CellPointerArrayOutOfBounds {
1007        start: usize,
1008        len: usize,
1009        usable_size: usize,
1010    },
1011    /// A cell pointer was invalid.
1012    InvalidCellPointer {
1013        index: usize,
1014        offset: u16,
1015        usable_size: usize,
1016    },
1017    /// Freeblock offset/size was invalid.
1018    InvalidFreeblock {
1019        offset: u16,
1020        size: u16,
1021        usable_size: usize,
1022    },
1023    /// Freeblock list contained a loop.
1024    FreeblockLoop { offset: u16 },
1025    /// Interior page right-most child pointer was invalid.
1026    InvalidRightMostChild { raw: u32 },
1027}
1028
1029impl fmt::Display for BTreePageError {
1030    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1031        match self {
1032            Self::PageSizeMismatch { expected, actual } => write!(
1033                f,
1034                "page size mismatch: expected {expected} bytes, got {actual} bytes"
1035            ),
1036            Self::PageTooSmall {
1037                usable_size,
1038                needed,
1039            } => write!(
1040                f,
1041                "page too small: usable_size={usable_size} needed={needed}"
1042            ),
1043            Self::InvalidPageType { raw } => write!(f, "invalid B-tree page type: {raw:#04x}"),
1044            Self::InvalidFragmentedFreeBytes { raw, max } => {
1045                write!(f, "invalid fragmented free bytes: {raw} (max {max})")
1046            }
1047            Self::InvalidCellContentAreaStart {
1048                raw,
1049                decoded,
1050                usable_size,
1051            } => write!(
1052                f,
1053                "invalid cell content area start: raw={raw} decoded={decoded} usable_size={usable_size}"
1054            ),
1055            Self::CellContentAreaOverlapsCellPointers {
1056                cell_content_start,
1057                cell_pointer_array_end,
1058            } => write!(
1059                f,
1060                "cell content area overlaps cell pointer array: cell_content_start={cell_content_start} cell_pointer_array_end={cell_pointer_array_end}"
1061            ),
1062            Self::CellPointerArrayOutOfBounds {
1063                start,
1064                len,
1065                usable_size,
1066            } => write!(
1067                f,
1068                "cell pointer array out of bounds: start={start} len={len} usable_size={usable_size}"
1069            ),
1070            Self::InvalidCellPointer {
1071                index,
1072                offset,
1073                usable_size,
1074            } => write!(
1075                f,
1076                "invalid cell pointer: index={index} offset={offset} usable_size={usable_size}"
1077            ),
1078            Self::InvalidFreeblock {
1079                offset,
1080                size,
1081                usable_size,
1082            } => write!(
1083                f,
1084                "invalid freeblock: offset={offset} size={size} usable_size={usable_size}"
1085            ),
1086            Self::FreeblockLoop { offset } => write!(f, "freeblock loop at offset {offset}"),
1087            Self::InvalidRightMostChild { raw } => {
1088                write!(f, "invalid right-most child pointer: {raw}")
1089            }
1090        }
1091    }
1092}
1093
1094impl std::error::Error for BTreePageError {}
1095
1096/// Parsed B-tree page header.
1097#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1098pub struct BTreePageHeader {
1099    /// Offset within the page where the B-tree page header begins (0 normally, 100 for page 1).
1100    pub header_offset: usize,
1101    /// Page type.
1102    pub page_type: BTreePageType,
1103    /// Offset of the first freeblock in the freeblock list (0 if none).
1104    pub first_freeblock: u16,
1105    /// Number of cells on this page.
1106    pub cell_count: u16,
1107    /// Start of cell content area. A raw value of 0 decodes to 65536.
1108    pub cell_content_start: u32,
1109    /// Count of fragmented free bytes on this page.
1110    pub fragmented_free_bytes: u8,
1111    /// Right-most child page number for interior pages.
1112    pub right_most_child: Option<PageNumber>,
1113}
1114
1115impl BTreePageHeader {
1116    /// Size of the B-tree page header in bytes (8 for leaf, 12 for interior).
1117    pub const fn header_size(self) -> usize {
1118        if self.page_type.is_leaf() { 8 } else { 12 }
1119    }
1120
1121    /// Parse a B-tree page header from a page buffer.
1122    pub fn parse(
1123        page: &[u8],
1124        page_size: PageSize,
1125        reserved_per_page: u8,
1126        is_page1: bool,
1127    ) -> Result<Self, BTreePageError> {
1128        let expected = page_size.as_usize();
1129        if page.len() != expected {
1130            return Err(BTreePageError::PageSizeMismatch {
1131                expected,
1132                actual: page.len(),
1133            });
1134        }
1135
1136        let usable_size = page_size.usable(reserved_per_page) as usize;
1137        let header_offset = if is_page1 { DATABASE_HEADER_SIZE } else { 0 };
1138        let min_needed = header_offset + 8;
1139        if usable_size < min_needed {
1140            return Err(BTreePageError::PageTooSmall {
1141                usable_size,
1142                needed: min_needed,
1143            });
1144        }
1145
1146        let page_type_raw = page[header_offset];
1147        let page_type = BTreePageType::from_byte(page_type_raw)
1148            .ok_or(BTreePageError::InvalidPageType { raw: page_type_raw })?;
1149
1150        let header_size = if page_type.is_leaf() { 8 } else { 12 };
1151        let needed = header_offset + header_size;
1152        if usable_size < needed {
1153            return Err(BTreePageError::PageTooSmall {
1154                usable_size,
1155                needed,
1156            });
1157        }
1158
1159        let first_freeblock =
1160            u16::from_be_bytes([page[header_offset + 1], page[header_offset + 2]]);
1161        let cell_count = u16::from_be_bytes([page[header_offset + 3], page[header_offset + 4]]);
1162        let cell_content_raw =
1163            u16::from_be_bytes([page[header_offset + 5], page[header_offset + 6]]);
1164        let cell_content_start = if cell_content_raw == 0 {
1165            65_536
1166        } else {
1167            u32::from(cell_content_raw)
1168        };
1169        let usable_size_u32 = u32::try_from(usable_size).unwrap_or(u32::MAX);
1170        if cell_content_start > usable_size_u32 {
1171            return Err(BTreePageError::InvalidCellContentAreaStart {
1172                raw: cell_content_raw,
1173                decoded: cell_content_start,
1174                usable_size,
1175            });
1176        }
1177
1178        let fragmented_free_bytes = page[header_offset + 7];
1179        if fragmented_free_bytes > BTREE_MAX_FRAGMENTED_FREE_BYTES {
1180            return Err(BTreePageError::InvalidFragmentedFreeBytes {
1181                raw: fragmented_free_bytes,
1182                max: BTREE_MAX_FRAGMENTED_FREE_BYTES,
1183            });
1184        }
1185
1186        let right_most_child = if page_type.is_interior() {
1187            let raw = u32::from_be_bytes([
1188                page[header_offset + 8],
1189                page[header_offset + 9],
1190                page[header_offset + 10],
1191                page[header_offset + 11],
1192            ]);
1193            let pn = PageNumber::new(raw).ok_or(BTreePageError::InvalidRightMostChild { raw })?;
1194            Some(pn)
1195        } else {
1196            None
1197        };
1198
1199        // Ensure the cell pointer array is within the usable page area.
1200        let ptr_array_start = header_offset + header_size;
1201        let ptr_array_len = usize::from(cell_count) * 2;
1202        if ptr_array_start + ptr_array_len > usable_size {
1203            return Err(BTreePageError::CellPointerArrayOutOfBounds {
1204                start: ptr_array_start,
1205                len: ptr_array_len,
1206                usable_size,
1207            });
1208        }
1209        let ptr_array_end = ptr_array_start + ptr_array_len;
1210        let ptr_array_end_u32 = u32::try_from(ptr_array_end).unwrap_or(u32::MAX);
1211        if cell_content_start < ptr_array_end_u32 {
1212            return Err(BTreePageError::CellContentAreaOverlapsCellPointers {
1213                cell_content_start,
1214                cell_pointer_array_end: ptr_array_end,
1215            });
1216        }
1217
1218        Ok(Self {
1219            header_offset,
1220            page_type,
1221            first_freeblock,
1222            cell_count,
1223            cell_content_start,
1224            fragmented_free_bytes,
1225            right_most_child,
1226        })
1227    }
1228
1229    /// Parse the cell pointer array for this page.
1230    pub fn parse_cell_pointers(
1231        self,
1232        page: &[u8],
1233        page_size: PageSize,
1234        reserved_per_page: u8,
1235    ) -> Result<Vec<u16>, BTreePageError> {
1236        let expected = page_size.as_usize();
1237        if page.len() != expected {
1238            return Err(BTreePageError::PageSizeMismatch {
1239                expected,
1240                actual: page.len(),
1241            });
1242        }
1243
1244        let usable_size = page_size.usable(reserved_per_page) as usize;
1245        let ptr_array_start = self.header_offset + self.header_size();
1246        let ptr_array_len = usize::from(self.cell_count) * 2;
1247        if ptr_array_start + ptr_array_len > usable_size {
1248            return Err(BTreePageError::CellPointerArrayOutOfBounds {
1249                start: ptr_array_start,
1250                len: ptr_array_len,
1251                usable_size,
1252            });
1253        }
1254
1255        let min_cell_offset = ptr_array_start + ptr_array_len;
1256        let mut out = Vec::with_capacity(self.cell_count as usize);
1257        for i in 0..self.cell_count as usize {
1258            let off = ptr_array_start + i * 2;
1259            let cell_off = u16::from_be_bytes([page[off], page[off + 1]]);
1260            let cell_off_usize = usize::from(cell_off);
1261            if cell_off_usize < min_cell_offset
1262                || cell_off_usize < self.cell_content_start as usize
1263                || cell_off_usize >= usable_size
1264            {
1265                return Err(BTreePageError::InvalidCellPointer {
1266                    index: i,
1267                    offset: cell_off,
1268                    usable_size,
1269                });
1270            }
1271            out.push(cell_off);
1272        }
1273        Ok(out)
1274    }
1275
1276    /// Traverse and parse the freeblock list for this page.
1277    pub fn parse_freeblocks(
1278        self,
1279        page: &[u8],
1280        page_size: PageSize,
1281        reserved_per_page: u8,
1282    ) -> Result<Vec<Freeblock>, BTreePageError> {
1283        let expected = page_size.as_usize();
1284        if page.len() != expected {
1285            return Err(BTreePageError::PageSizeMismatch {
1286                expected,
1287                actual: page.len(),
1288            });
1289        }
1290        let usable_size = page_size.usable(reserved_per_page) as usize;
1291
1292        let mut blocks = Vec::new();
1293        let mut seen = std::collections::BTreeSet::new();
1294        let mut offset = self.first_freeblock;
1295        while offset != 0 {
1296            if !seen.insert(offset) {
1297                return Err(BTreePageError::FreeblockLoop { offset });
1298            }
1299
1300            let off = usize::from(offset);
1301            if off < self.cell_content_start as usize {
1302                return Err(BTreePageError::InvalidFreeblock {
1303                    offset,
1304                    size: 0,
1305                    usable_size,
1306                });
1307            }
1308            if off + 4 > usable_size {
1309                return Err(BTreePageError::InvalidFreeblock {
1310                    offset,
1311                    size: 0,
1312                    usable_size,
1313                });
1314            }
1315
1316            let next = u16::from_be_bytes([page[off], page[off + 1]]);
1317            let size = u16::from_be_bytes([page[off + 2], page[off + 3]]);
1318            if size < 4 || off + usize::from(size) > usable_size {
1319                return Err(BTreePageError::InvalidFreeblock {
1320                    offset,
1321                    size,
1322                    usable_size,
1323                });
1324            }
1325
1326            blocks.push(Freeblock { offset, next, size });
1327            offset = next;
1328        }
1329
1330        Ok(blocks)
1331    }
1332
1333    /// Write an empty leaf-table B-tree page header into a buffer.
1334    ///
1335    /// Sets up the 8-byte B-tree page header for an empty leaf table page
1336    /// (type `0x0D`) with zero cells, suitable for `sqlite_master` or any
1337    /// newly created table root page.
1338    ///
1339    /// `header_offset` is the byte offset of the B-tree header within the
1340    /// page buffer.  For page 1 this must be [`DATABASE_HEADER_SIZE`] (100);
1341    /// for every other page it should be 0.
1342    ///
1343    /// `usable_size` equals `page_size − reserved_per_page`.  The cell
1344    /// content area offset is set to this value so that all usable space is
1345    /// available for future cell insertions.
1346    #[allow(clippy::cast_possible_truncation)]
1347    pub fn write_empty_leaf_table(page: &mut [u8], header_offset: usize, usable_size: u32) {
1348        page[header_offset] = BTreePageType::LeafTable as u8; // 0x0D
1349        // first_freeblock = 0 (no freeblocks)
1350        page[header_offset + 1] = 0;
1351        page[header_offset + 2] = 0;
1352        // cell_count = 0
1353        page[header_offset + 3] = 0;
1354        page[header_offset + 4] = 0;
1355        // cell content area offset (0 encodes 65536)
1356        let content_raw = if usable_size >= 65_536 {
1357            0u16
1358        } else {
1359            usable_size as u16
1360        };
1361        page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1362        // fragmented_free_bytes = 0
1363        page[header_offset + 7] = 0;
1364    }
1365
1366    /// Initialize an empty leaf index page (type `0x0A`) with zero cells,
1367    /// suitable for a newly created index root page.
1368    ///
1369    /// `header_offset` is the byte offset of the B-tree header within the
1370    /// page buffer (0 for all non-page-1 pages).
1371    ///
1372    /// `usable_size` equals `page_size − reserved_per_page`.
1373    #[allow(clippy::cast_possible_truncation)]
1374    pub fn write_empty_leaf_index(page: &mut [u8], header_offset: usize, usable_size: u32) {
1375        page[header_offset] = BTreePageType::LeafIndex as u8; // 0x0A
1376        // first_freeblock = 0 (no freeblocks)
1377        page[header_offset + 1] = 0;
1378        page[header_offset + 2] = 0;
1379        // cell_count = 0
1380        page[header_offset + 3] = 0;
1381        page[header_offset + 4] = 0;
1382        // cell content area offset (0 encodes 65536)
1383        let content_raw = if usable_size >= 65_536 {
1384            0u16
1385        } else {
1386            usable_size as u16
1387        };
1388        page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1389        // fragmented_free_bytes = 0
1390        page[header_offset + 7] = 0;
1391    }
1392}
1393
1394/// A freeblock entry in a B-tree page freeblock list.
1395#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1396pub struct Freeblock {
1397    pub offset: u16,
1398    pub next: u16,
1399    pub size: u16,
1400}
1401
1402/// Determine if adding `additional` fragmented bytes would exceed the maximum allowed.
1403pub const fn would_exceed_fragmented_free_bytes(current: u8, additional: u8) -> bool {
1404    current.saturating_add(additional) > BTREE_MAX_FRAGMENTED_FREE_BYTES
1405}
1406
1407/// B-tree page types.
1408#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1409#[repr(u8)]
1410pub enum BTreePageType {
1411    /// Interior index B-tree page.
1412    InteriorIndex = 2,
1413    /// Interior table B-tree page.
1414    InteriorTable = 5,
1415    /// Leaf index B-tree page.
1416    LeafIndex = 10,
1417    /// Leaf table B-tree page.
1418    LeafTable = 13,
1419}
1420
1421impl BTreePageType {
1422    /// Parse from the raw byte value at the start of a B-tree page header.
1423    pub const fn from_byte(b: u8) -> Option<Self> {
1424        match b {
1425            2 => Some(Self::InteriorIndex),
1426            5 => Some(Self::InteriorTable),
1427            10 => Some(Self::LeafIndex),
1428            13 => Some(Self::LeafTable),
1429            _ => None,
1430        }
1431    }
1432
1433    /// Whether this is a leaf page (no children).
1434    pub const fn is_leaf(self) -> bool {
1435        matches!(self, Self::LeafIndex | Self::LeafTable)
1436    }
1437
1438    /// Whether this is an interior (non-leaf) page.
1439    pub const fn is_interior(self) -> bool {
1440        matches!(self, Self::InteriorIndex | Self::InteriorTable)
1441    }
1442
1443    /// Whether this is a table B-tree (INTKEY) page.
1444    pub const fn is_table(self) -> bool {
1445        matches!(self, Self::InteriorTable | Self::LeafTable)
1446    }
1447
1448    /// Whether this is an index B-tree (BLOBKEY) page.
1449    pub const fn is_index(self) -> bool {
1450        matches!(self, Self::InteriorIndex | Self::LeafIndex)
1451    }
1452}
1453
1454#[cfg(test)]
1455mod tests {
1456    use super::*;
1457    use std::sync::Arc;
1458
1459    #[test]
1460    fn page_number_zero_is_invalid() {
1461        assert!(PageNumber::new(0).is_none());
1462        assert!(PageNumber::try_from(0u32).is_err());
1463    }
1464
1465    #[test]
1466    fn test_page_number_zero_rejected() {
1467        assert!(PageNumber::new(0).is_none());
1468        assert!(PageNumber::try_from(0u32).is_err());
1469    }
1470
1471    #[test]
1472    fn page_number_valid() {
1473        let pn = PageNumber::new(1).unwrap();
1474        assert_eq!(pn.get(), 1);
1475        assert_eq!(pn, PageNumber::ONE);
1476
1477        let pn = PageNumber::new(42).unwrap();
1478        assert_eq!(pn.get(), 42);
1479        assert_eq!(pn.to_string(), "42");
1480    }
1481
1482    #[test]
1483    fn page_number_ordering() {
1484        let a = PageNumber::new(1).unwrap();
1485        let b = PageNumber::new(100).unwrap();
1486        assert!(a < b);
1487    }
1488
1489    #[test]
1490    fn page_size_validation() {
1491        assert!(PageSize::new(0).is_none());
1492        assert!(PageSize::new(256).is_none());
1493        assert!(PageSize::new(511).is_none());
1494        assert!(PageSize::new(513).is_none());
1495        assert!(PageSize::new(1000).is_none());
1496        assert!(PageSize::new(131_072).is_none());
1497
1498        assert!(PageSize::new(512).is_some());
1499        assert!(PageSize::new(1024).is_some());
1500        assert!(PageSize::new(4096).is_some());
1501        assert!(PageSize::new(8192).is_some());
1502        assert!(PageSize::new(16384).is_some());
1503        assert!(PageSize::new(32768).is_some());
1504        assert!(PageSize::new(65536).is_some());
1505    }
1506
1507    #[test]
1508    fn page_size_defaults() {
1509        assert_eq!(PageSize::DEFAULT.get(), 4096);
1510        assert_eq!(PageSize::MIN.get(), 512);
1511        assert_eq!(PageSize::MAX.get(), 65536);
1512        assert_eq!(PageSize::default(), PageSize::DEFAULT);
1513    }
1514
1515    fn make_header_for_tests() -> DatabaseHeader {
1516        DatabaseHeader {
1517            page_size: PageSize::DEFAULT,
1518            write_version: 2,
1519            read_version: 2,
1520            reserved_per_page: 0,
1521            change_counter: 7,
1522            page_count: 123,
1523            freelist_trunk: 0,
1524            freelist_count: 0,
1525            schema_cookie: 1,
1526            schema_format: 4,
1527            default_cache_size: -2000,
1528            largest_root_page: 0,
1529            text_encoding: TextEncoding::Utf8,
1530            user_version: 0,
1531            incremental_vacuum: 0,
1532            application_id: 0,
1533            version_valid_for: 7,
1534            sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
1535        }
1536    }
1537
1538    #[test]
1539    fn test_header_magic_validation() {
1540        let hdr = make_header_for_tests();
1541        let mut buf = hdr.to_bytes().unwrap();
1542        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
1543        assert_eq!(parsed, hdr);
1544
1545        buf[0] = b'X';
1546        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1547        assert!(matches!(err, DatabaseHeaderError::InvalidMagic));
1548    }
1549
1550    #[test]
1551    fn test_header_page_size_encoding() {
1552        // 65536 is encoded as 1.
1553        let mut hdr = make_header_for_tests();
1554        hdr.page_size = PageSize::new(65_536).unwrap();
1555        let buf = hdr.to_bytes().unwrap();
1556        assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), 1);
1557        assert_eq!(
1558            DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1559            65_536
1560        );
1561
1562        // Typical values are stored literally.
1563        for size in [512u32, 1024, 2048, 4096, 8192, 16_384, 32_768] {
1564            hdr.page_size = PageSize::new(size).unwrap();
1565            let buf = hdr.to_bytes().unwrap();
1566            let expected_u16 = u16::try_from(size).unwrap();
1567            assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), expected_u16);
1568            assert_eq!(
1569                DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1570                size
1571            );
1572        }
1573
1574        // Non power-of-two rejected.
1575        let mut buf = make_header_for_tests().to_bytes().unwrap();
1576        buf[16..18].copy_from_slice(&1000u16.to_be_bytes());
1577        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1578        assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1579    }
1580
1581    #[test]
1582    fn test_header_page_size_range() {
1583        let mut buf = make_header_for_tests().to_bytes().unwrap();
1584        buf[16..18].copy_from_slice(&256u16.to_be_bytes());
1585        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1586        assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1587    }
1588
1589    #[test]
1590    fn test_header_write_read_version() {
1591        let mut hdr = make_header_for_tests();
1592
1593        hdr.write_version = 2;
1594        hdr.read_version = 2;
1595        assert_eq!(
1596            hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1597            DatabaseOpenMode::ReadWrite
1598        );
1599
1600        hdr.read_version = 3;
1601        let err = hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap_err();
1602        assert!(matches!(
1603            err,
1604            DatabaseHeaderError::UnsupportedReadVersion { .. }
1605        ));
1606
1607        hdr.read_version = 2;
1608        hdr.write_version = 3;
1609        assert_eq!(
1610            hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1611            DatabaseOpenMode::ReadOnly
1612        );
1613    }
1614
1615    #[test]
1616    fn test_header_payload_fractions() {
1617        let mut buf = make_header_for_tests().to_bytes().unwrap();
1618        buf[21] = 65;
1619        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1620        assert!(matches!(
1621            err,
1622            DatabaseHeaderError::InvalidPayloadFractions { .. }
1623        ));
1624    }
1625
1626    #[test]
1627    fn test_header_usable_size_minimum() {
1628        // For 512-byte pages, reserved_per_page must be <= 32 (512-32=480).
1629        let mut buf = make_header_for_tests().to_bytes().unwrap();
1630        buf[16..18].copy_from_slice(&512u16.to_be_bytes());
1631        buf[20] = 33;
1632        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1633        assert!(matches!(
1634            err,
1635            DatabaseHeaderError::UsableSizeTooSmall { .. }
1636        ));
1637
1638        buf[20] = 32;
1639        DatabaseHeader::from_bytes(&buf).unwrap();
1640    }
1641
1642    #[test]
1643    fn test_header_round_trip() {
1644        let hdr = make_header_for_tests();
1645        let buf1 = hdr.to_bytes().unwrap();
1646        let parsed = DatabaseHeader::from_bytes(&buf1).unwrap();
1647        assert_eq!(parsed, hdr);
1648
1649        let buf2 = parsed.to_bytes().unwrap();
1650        assert_eq!(buf1, buf2);
1651    }
1652
1653    #[test]
1654    fn test_btree_page_header_leaf() {
1655        let page_size = PageSize::new(512).unwrap();
1656        let mut page = vec![0u8; page_size.as_usize()];
1657
1658        // Leaf table page.
1659        page[0] = 0x0D;
1660        page[1..3].copy_from_slice(&0u16.to_be_bytes()); // first freeblock
1661        page[3..5].copy_from_slice(&1u16.to_be_bytes()); // 1 cell
1662        page[5..7].copy_from_slice(&400u16.to_be_bytes()); // cell content start
1663        page[7] = 0; // fragmented bytes
1664
1665        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1666        assert!(hdr.page_type.is_leaf());
1667        assert_eq!(hdr.header_size(), 8);
1668    }
1669
1670    #[test]
1671    fn test_btree_page_header_interior() {
1672        let page_size = PageSize::new(512).unwrap();
1673        let mut page = vec![0u8; page_size.as_usize()];
1674
1675        // Interior table page.
1676        page[0] = 0x05;
1677        page[1..3].copy_from_slice(&0u16.to_be_bytes());
1678        page[3..5].copy_from_slice(&0u16.to_be_bytes());
1679        page[5..7].copy_from_slice(&500u16.to_be_bytes());
1680        page[7] = 0;
1681        page[8..12].copy_from_slice(&2u32.to_be_bytes()); // right-most child
1682
1683        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1684        assert!(hdr.page_type.is_interior());
1685        assert_eq!(hdr.header_size(), 12);
1686        assert_eq!(hdr.right_most_child.unwrap().get(), 2);
1687    }
1688
1689    #[test]
1690    fn test_page1_offset_adjustment() {
1691        let page_size = PageSize::new(512).unwrap();
1692        let mut page = vec![0u8; page_size.as_usize()];
1693
1694        // Page 1: B-tree header starts after the 100-byte DB header prefix.
1695        let h = DATABASE_HEADER_SIZE;
1696        page[h] = 0x0D; // leaf table
1697        page[h + 1..h + 3].copy_from_slice(&0u16.to_be_bytes());
1698        page[h + 3..h + 5].copy_from_slice(&1u16.to_be_bytes()); // 1 cell
1699        page[h + 5..h + 7].copy_from_slice(&300u16.to_be_bytes()); // cell content start
1700        page[h + 7] = 0;
1701
1702        // Cell pointer array begins at h+8.
1703        page[h + 8..h + 10].copy_from_slice(&300u16.to_be_bytes());
1704
1705        let hdr = BTreePageHeader::parse(&page, page_size, 0, true).unwrap();
1706        let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
1707        assert_eq!(ptrs, vec![300u16]);
1708    }
1709
1710    #[test]
1711    fn test_cell_pointer_array() {
1712        let page_size = PageSize::new(512).unwrap();
1713        let mut page = vec![0u8; page_size.as_usize()];
1714
1715        page[0] = 0x0D;
1716        page[1..3].copy_from_slice(&0u16.to_be_bytes());
1717        page[3..5].copy_from_slice(&3u16.to_be_bytes()); // 3 cells
1718        page[5..7].copy_from_slice(&300u16.to_be_bytes());
1719        page[7] = 0;
1720        page[8..10].copy_from_slice(&300u16.to_be_bytes());
1721        page[10..12].copy_from_slice(&320u16.to_be_bytes());
1722        page[12..14].copy_from_slice(&340u16.to_be_bytes());
1723
1724        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1725        let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
1726        assert_eq!(ptrs, vec![300u16, 320u16, 340u16]);
1727    }
1728
1729    #[test]
1730    fn test_freeblock_list_traversal() {
1731        let page_size = PageSize::new(512).unwrap();
1732        let mut page = vec![0u8; page_size.as_usize()];
1733
1734        page[0] = 0x0D;
1735        page[1..3].copy_from_slice(&400u16.to_be_bytes()); // first freeblock
1736        page[3..5].copy_from_slice(&0u16.to_be_bytes());
1737        page[5..7].copy_from_slice(&400u16.to_be_bytes());
1738        page[7] = 0;
1739
1740        // freeblock at 400 -> next 420, size 20
1741        page[400..402].copy_from_slice(&420u16.to_be_bytes());
1742        page[402..404].copy_from_slice(&20u16.to_be_bytes());
1743        // freeblock at 420 -> next 0, size 30
1744        page[420..422].copy_from_slice(&0u16.to_be_bytes());
1745        page[422..424].copy_from_slice(&30u16.to_be_bytes());
1746
1747        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1748        let blocks = hdr.parse_freeblocks(&page, page_size, 0).unwrap();
1749        assert_eq!(
1750            blocks,
1751            vec![
1752                Freeblock {
1753                    offset: 400,
1754                    next: 420,
1755                    size: 20
1756                },
1757                Freeblock {
1758                    offset: 420,
1759                    next: 0,
1760                    size: 30
1761                }
1762            ]
1763        );
1764    }
1765
1766    #[test]
1767    fn test_freeblock_min_size() {
1768        let page_size = PageSize::new(512).unwrap();
1769        let mut page = vec![0u8; page_size.as_usize()];
1770
1771        page[0] = 0x0D;
1772        page[1..3].copy_from_slice(&400u16.to_be_bytes());
1773        page[3..5].copy_from_slice(&0u16.to_be_bytes());
1774        page[5..7].copy_from_slice(&400u16.to_be_bytes());
1775        page[7] = 0;
1776
1777        page[400..402].copy_from_slice(&0u16.to_be_bytes());
1778        page[402..404].copy_from_slice(&3u16.to_be_bytes()); // invalid
1779
1780        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1781        let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
1782        assert!(matches!(err, BTreePageError::InvalidFreeblock { .. }));
1783    }
1784
1785    #[test]
1786    fn test_fragment_defrag_threshold() {
1787        assert!(!would_exceed_fragmented_free_bytes(60, 0));
1788        assert!(would_exceed_fragmented_free_bytes(60, 1));
1789        assert!(would_exceed_fragmented_free_bytes(59, 2));
1790    }
1791
1792    #[test]
1793    fn test_e2e_bd_1a32() {
1794        use std::fs::File;
1795        use std::io::{Read, Seek};
1796        use std::process::Command;
1797        use std::sync::atomic::{AtomicUsize, Ordering};
1798
1799        static COUNTER: AtomicUsize = AtomicUsize::new(0);
1800
1801        // If sqlite3 isn't available in the environment, skip.
1802        if Command::new("sqlite3").arg("--version").output().is_err() {
1803            return;
1804        }
1805
1806        let mut path = std::env::temp_dir();
1807        path.push(format!(
1808            "fsqlite_bd_1a32_{}_{}.sqlite",
1809            std::process::id(),
1810            COUNTER.fetch_add(1, Ordering::Relaxed)
1811        ));
1812
1813        let status = Command::new("sqlite3")
1814            .arg(&path)
1815            .arg("CREATE TABLE t(x); INSERT INTO t VALUES(1);")
1816            .status()
1817            .expect("sqlite3 execution failed");
1818        assert!(status.success());
1819
1820        let mut f = File::open(&path).expect("open temp db");
1821        let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
1822        f.read_exact(&mut header_bytes).expect("read db header");
1823        let header = DatabaseHeader::from_bytes(&header_bytes).expect("parse db header");
1824        assert_eq!(header.schema_format, 4);
1825        assert_eq!(
1826            header.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1827            DatabaseOpenMode::ReadWrite
1828        );
1829
1830        // Re-serialize the parsed header and verify byte-for-byte equivalence.
1831        let hdr2 = header.to_bytes().expect("serialize header");
1832        assert_eq!(header_bytes, hdr2);
1833
1834        // Parse page 1 B-tree header from the first page.
1835        let page_size = header.page_size;
1836        let mut page1 = vec![0u8; page_size.as_usize()];
1837        f.rewind().expect("rewind");
1838        f.read_exact(&mut page1).expect("read page 1");
1839        let btree_hdr = BTreePageHeader::parse(&page1, page_size, header.reserved_per_page, true)
1840            .expect("parse page1 btree header");
1841        assert_eq!(btree_hdr.header_offset, DATABASE_HEADER_SIZE);
1842    }
1843
1844    #[test]
1845    fn test_varint_signed_cast() {
1846        use crate::serial_type::{read_varint, write_varint};
1847
1848        // Varint-decoded u64 cast to i64 produces correct two's complement for rowids.
1849        let test_cases: &[(u64, i64)] = &[
1850            (0, 0),
1851            (1, 1),
1852            (0x7FFF_FFFF_FFFF_FFFF, i64::MAX),
1853            (u64::MAX, -1),
1854            (0x8000_0000_0000_0000, i64::MIN),
1855        ];
1856        let mut buf = [0u8; 9];
1857        for &(unsigned, expected_signed) in test_cases {
1858            let written = write_varint(&mut buf, unsigned);
1859            let (decoded, consumed) = read_varint(&buf[..written]).unwrap();
1860            assert_eq!(decoded, unsigned);
1861            assert_eq!(consumed, written);
1862            #[allow(clippy::cast_possible_wrap)]
1863            let signed = decoded as i64;
1864            assert_eq!(
1865                signed, expected_signed,
1866                "u64 {unsigned} should cast to i64 {expected_signed}, got {signed}"
1867            );
1868        }
1869    }
1870
1871    #[test]
1872    fn test_reserved_bytes_72_91_zero() {
1873        let hdr = make_header_for_tests();
1874        let buf = hdr.to_bytes().unwrap();
1875        for (i, &byte) in buf.iter().enumerate().take(92).skip(72) {
1876            assert_eq!(byte, 0, "byte {i} should be zero (reserved region)");
1877        }
1878
1879        let mut hdr2 = make_header_for_tests();
1880        hdr2.application_id = 0xDEAD_BEEF;
1881        hdr2.user_version = 42;
1882        let buf2 = hdr2.to_bytes().unwrap();
1883        for (i, &byte) in buf2.iter().enumerate().take(92).skip(72) {
1884            assert_eq!(byte, 0, "byte {i} should be zero even with custom app_id");
1885        }
1886    }
1887
1888    #[test]
1889    fn test_version_valid_for_stale() {
1890        let mut hdr = make_header_for_tests();
1891        hdr.change_counter = 7;
1892        hdr.version_valid_for = 7;
1893        assert!(!hdr.is_page_count_stale());
1894
1895        hdr.version_valid_for = 5;
1896        assert!(hdr.is_page_count_stale());
1897
1898        hdr.page_size = PageSize::new(4096).unwrap();
1899        assert_eq!(hdr.page_count_from_file_size(4096 * 100), Some(100));
1900        assert_eq!(hdr.page_count_from_file_size(4096), Some(1));
1901        assert!(hdr.page_count_from_file_size(5000).is_none());
1902        assert!(hdr.page_count_from_file_size(0).is_none());
1903    }
1904
1905    #[test]
1906    fn test_reserved_space_per_page() {
1907        let mut hdr = make_header_for_tests();
1908        hdr.page_size = PageSize::new(4096).unwrap();
1909        hdr.reserved_per_page = 40;
1910        let usable = hdr.page_size.usable(hdr.reserved_per_page);
1911        assert_eq!(usable, 4056);
1912
1913        let buf = hdr.to_bytes().unwrap();
1914        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
1915        assert_eq!(parsed.reserved_per_page, 40);
1916        assert_eq!(parsed.page_size.usable(parsed.reserved_per_page), 4056);
1917    }
1918
1919    #[test]
1920    fn test_header_text_encoding_invalid() {
1921        let mut buf = make_header_for_tests().to_bytes().unwrap();
1922        buf[56..60].copy_from_slice(&4u32.to_be_bytes());
1923        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1924        assert!(matches!(
1925            err,
1926            DatabaseHeaderError::InvalidTextEncoding { raw: 4 }
1927        ));
1928
1929        buf[56..60].copy_from_slice(&0u32.to_be_bytes());
1930        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1931        assert!(matches!(
1932            err,
1933            DatabaseHeaderError::InvalidTextEncoding { raw: 0 }
1934        ));
1935    }
1936
1937    #[test]
1938    fn test_btree_page_type_classification() {
1939        assert_eq!(
1940            BTreePageType::from_byte(0x02),
1941            Some(BTreePageType::InteriorIndex)
1942        );
1943        assert_eq!(
1944            BTreePageType::from_byte(0x05),
1945            Some(BTreePageType::InteriorTable)
1946        );
1947        assert_eq!(
1948            BTreePageType::from_byte(0x0A),
1949            Some(BTreePageType::LeafIndex)
1950        );
1951        assert_eq!(
1952            BTreePageType::from_byte(0x0D),
1953            Some(BTreePageType::LeafTable)
1954        );
1955
1956        assert!(BTreePageType::from_byte(0x00).is_none());
1957        assert!(BTreePageType::from_byte(0x01).is_none());
1958        assert!(BTreePageType::from_byte(0xFF).is_none());
1959
1960        assert!(BTreePageType::InteriorTable.is_interior());
1961        assert!(BTreePageType::InteriorTable.is_table());
1962        assert!(!BTreePageType::InteriorTable.is_leaf());
1963        assert!(!BTreePageType::InteriorTable.is_index());
1964
1965        assert!(BTreePageType::LeafIndex.is_leaf());
1966        assert!(BTreePageType::LeafIndex.is_index());
1967        assert!(!BTreePageType::LeafIndex.is_interior());
1968        assert!(!BTreePageType::LeafIndex.is_table());
1969    }
1970
1971    #[test]
1972    fn test_invalid_page_type_rejected() {
1973        let page_size = PageSize::new(512).unwrap();
1974        let mut page = vec![0u8; page_size.as_usize()];
1975        page[0] = 0x01;
1976        let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
1977        assert!(matches!(err, BTreePageError::InvalidPageType { raw: 0x01 }));
1978    }
1979
1980    #[test]
1981    fn test_freeblock_loop_detected() {
1982        let page_size = PageSize::new(512).unwrap();
1983        let mut page = vec![0u8; page_size.as_usize()];
1984
1985        page[0] = 0x0D;
1986        page[1..3].copy_from_slice(&400u16.to_be_bytes()); // first freeblock
1987        page[3..5].copy_from_slice(&0u16.to_be_bytes()); // 0 cells
1988        // cell_content_start must be <= 400 so freeblocks are valid
1989        page[5..7].copy_from_slice(&300u16.to_be_bytes());
1990        page[7] = 0;
1991
1992        // freeblock at 400 -> next 420, size 20
1993        page[400..402].copy_from_slice(&420u16.to_be_bytes());
1994        page[402..404].copy_from_slice(&20u16.to_be_bytes());
1995        // freeblock at 420 -> next 400 (LOOP), size 20
1996        page[420..422].copy_from_slice(&400u16.to_be_bytes());
1997        page[422..424].copy_from_slice(&20u16.to_be_bytes());
1998
1999        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2000        let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
2001        assert!(matches!(err, BTreePageError::FreeblockLoop { .. }));
2002    }
2003
2004    #[test]
2005    fn test_fragmented_free_bytes_max() {
2006        let page_size = PageSize::new(512).unwrap();
2007        let mut page = vec![0u8; page_size.as_usize()];
2008
2009        page[0] = 0x0D;
2010        page[5..7].copy_from_slice(&500u16.to_be_bytes()); // valid cell_content_start
2011        page[7] = 61; // exceeds max of 60
2012        let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2013        assert!(matches!(
2014            err,
2015            BTreePageError::InvalidFragmentedFreeBytes { raw: 61, max: 60 }
2016        ));
2017
2018        // 60 is exactly the limit -- should succeed
2019        page[7] = 60;
2020        BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2021    }
2022
2023    #[test]
2024    fn test_error_variants_distinct_display() {
2025        let errors: Vec<DatabaseHeaderError> = vec![
2026            DatabaseHeaderError::InvalidMagic,
2027            DatabaseHeaderError::InvalidPageSize { raw: 100 },
2028            DatabaseHeaderError::InvalidPayloadFractions {
2029                max: 65,
2030                min: 32,
2031                leaf: 32,
2032            },
2033            DatabaseHeaderError::UsableSizeTooSmall {
2034                page_size: 512,
2035                reserved_per_page: 33,
2036                usable_size: 479,
2037            },
2038            DatabaseHeaderError::UnsupportedReadVersion {
2039                read_version: 3,
2040                max_supported: 2,
2041            },
2042            DatabaseHeaderError::InvalidTextEncoding { raw: 4 },
2043            DatabaseHeaderError::InvalidSchemaFormat { raw: 0 },
2044        ];
2045
2046        let displays: Vec<String> = errors
2047            .iter()
2048            .map(std::string::ToString::to_string)
2049            .collect();
2050        for (i, d) in displays.iter().enumerate() {
2051            assert!(!d.is_empty(), "error variant {i} has empty display");
2052            for (j, d2) in displays.iter().enumerate() {
2053                if i != j {
2054                    assert_ne!(d, d2, "error variants {i} and {j} have identical display");
2055                }
2056            }
2057        }
2058    }
2059
2060    // ── bd-94us §11.11-11.12 sqlite_master + encoding tests ────────────
2061
2062    #[test]
2063    fn test_sqlite_master_page1_root() {
2064        // sqlite_master is always rooted at page 1.
2065        // On creation, page 1 is a table leaf (0x0D) with 0 cells.
2066        let page_size = PageSize::new(4096).unwrap();
2067        let mut page = [0u8; 4096];
2068        // Page 1 has 100-byte database header prefix.
2069        // B-tree header starts at offset 100 for page 1.
2070        page[..16].copy_from_slice(b"SQLite format 3\0");
2071        page[16..18].copy_from_slice(&4096u16.to_be_bytes()); // page size
2072        page[100] = 0x0D; // leaf table page type at header offset
2073        // cell count = 0 at offset 103
2074        page[103..105].copy_from_slice(&0u16.to_be_bytes());
2075        // cell content area start = page_size at offset 105
2076        page[105..107].copy_from_slice(&4096u16.to_be_bytes()); // cell content area at end of page
2077
2078        let page_type = BTreePageType::from_byte(page[100]);
2079        assert_eq!(page_type, Some(BTreePageType::LeafTable));
2080        let hdr = BTreePageHeader::parse(&page, page_size, 0, true).expect("valid leaf header");
2081        assert_eq!(hdr.cell_count, 0, "fresh sqlite_master has 0 rows");
2082    }
2083
2084    #[test]
2085    fn test_sqlite_master_schema_columns() {
2086        // sqlite_master has exactly 5 columns: type, name, tbl_name, rootpage, sql.
2087        let columns = ["type", "name", "tbl_name", "rootpage", "sql"];
2088        assert_eq!(columns.len(), 5);
2089        // Verify the valid type values.
2090        let valid_types = ["table", "index", "view", "trigger"];
2091        assert_eq!(valid_types.len(), 4);
2092    }
2093
2094    #[test]
2095    fn test_encoding_utf8_default() {
2096        // New database defaults to text encoding 1 (UTF-8).
2097        let hdr = DatabaseHeader::default();
2098        assert_eq!(hdr.text_encoding, TextEncoding::Utf8);
2099
2100        let bytes = hdr.to_bytes().expect("encode");
2101        // Header offset 56 stores encoding as big-endian u32.
2102        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2103        assert_eq!(enc_raw, 1, "UTF-8 encoding stored as 1 at offset 56");
2104    }
2105
2106    #[test]
2107    fn test_encoding_utf16le() {
2108        let mut hdr = make_header_for_tests();
2109        hdr.text_encoding = TextEncoding::Utf16le;
2110        let bytes = hdr.to_bytes().expect("encode");
2111        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2112        assert_eq!(enc_raw, 2, "UTF-16LE encoding stored as 2");
2113
2114        let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2115        assert_eq!(parsed.text_encoding, TextEncoding::Utf16le);
2116    }
2117
2118    #[test]
2119    fn test_encoding_utf16be() {
2120        let mut hdr = make_header_for_tests();
2121        hdr.text_encoding = TextEncoding::Utf16be;
2122        let bytes = hdr.to_bytes().expect("encode");
2123        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2124        assert_eq!(enc_raw, 3, "UTF-16BE encoding stored as 3");
2125
2126        let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2127        assert_eq!(parsed.text_encoding, TextEncoding::Utf16be);
2128    }
2129
2130    #[test]
2131    fn test_encoding_immutable_after_creation() {
2132        // Encoding is set at creation and cannot be changed afterward.
2133        // Changing the encoding field in an existing header and re-serializing
2134        // produces a different byte at offset 56 -- the enforcement is at the
2135        // application layer (PRAGMA encoding is rejected after first table).
2136        let hdr1 = make_header_for_tests();
2137        assert_eq!(hdr1.text_encoding, TextEncoding::Utf8);
2138        let bytes1 = hdr1.to_bytes().expect("encode");
2139
2140        let mut hdr2 = hdr1;
2141        hdr2.text_encoding = TextEncoding::Utf16le;
2142        let bytes2 = hdr2.to_bytes().expect("encode");
2143
2144        // The encoding field differs in the serialized bytes.
2145        assert_ne!(
2146            bytes1[56..60],
2147            bytes2[56..60],
2148            "different encodings must serialize differently"
2149        );
2150    }
2151
2152    #[test]
2153    fn test_binary_collation_memcmp_utf8() {
2154        // BINARY collation uses memcmp on raw bytes.
2155        // For UTF-8, memcmp produces correct Unicode code point ordering.
2156        let a = "abc";
2157        let b = "abd";
2158        assert!(
2159            a.as_bytes() < b.as_bytes(),
2160            "memcmp ordering for ASCII UTF-8"
2161        );
2162
2163        // Multi-byte UTF-8: 'é' (U+00E9) = [0xC3, 0xA9], 'z' (U+007A) = [0x7A].
2164        // In code point order: 'z' (122) < 'é' (233).
2165        // In byte order: 0x7A < 0xC3, so 'z' < 'é' — same as code point order.
2166        let z = "z";
2167        let e_acute = "é";
2168        assert!(
2169            z.as_bytes() < e_acute.as_bytes(),
2170            "UTF-8 memcmp preserves code point order"
2171        );
2172    }
2173
2174    // ── bd-16ov §12.15-12.16 Type Affinity tests ────────────────────────
2175
2176    #[test]
2177    fn test_affinity_int_keyword() {
2178        assert_eq!(
2179            TypeAffinity::from_type_name("INTEGER"),
2180            TypeAffinity::Integer
2181        );
2182        assert_eq!(TypeAffinity::from_type_name("INT"), TypeAffinity::Integer);
2183        assert_eq!(
2184            TypeAffinity::from_type_name("TINYINT"),
2185            TypeAffinity::Integer
2186        );
2187        assert_eq!(
2188            TypeAffinity::from_type_name("SMALLINT"),
2189            TypeAffinity::Integer
2190        );
2191        assert_eq!(
2192            TypeAffinity::from_type_name("MEDIUMINT"),
2193            TypeAffinity::Integer
2194        );
2195        assert_eq!(
2196            TypeAffinity::from_type_name("BIGINT"),
2197            TypeAffinity::Integer
2198        );
2199        assert_eq!(
2200            TypeAffinity::from_type_name("UNSIGNED BIG INT"),
2201            TypeAffinity::Integer
2202        );
2203        assert_eq!(TypeAffinity::from_type_name("INT2"), TypeAffinity::Integer);
2204        assert_eq!(TypeAffinity::from_type_name("INT8"), TypeAffinity::Integer);
2205    }
2206
2207    #[test]
2208    fn test_affinity_text_keyword() {
2209        assert_eq!(TypeAffinity::from_type_name("TEXT"), TypeAffinity::Text);
2210        assert_eq!(
2211            TypeAffinity::from_type_name("CHARACTER(20)"),
2212            TypeAffinity::Text
2213        );
2214        assert_eq!(
2215            TypeAffinity::from_type_name("VARCHAR(255)"),
2216            TypeAffinity::Text
2217        );
2218        assert_eq!(
2219            TypeAffinity::from_type_name("VARYING CHARACTER(255)"),
2220            TypeAffinity::Text
2221        );
2222        assert_eq!(
2223            TypeAffinity::from_type_name("NCHAR(55)"),
2224            TypeAffinity::Text
2225        );
2226        assert_eq!(
2227            TypeAffinity::from_type_name("NATIVE CHARACTER(70)"),
2228            TypeAffinity::Text
2229        );
2230        assert_eq!(
2231            TypeAffinity::from_type_name("NVARCHAR(100)"),
2232            TypeAffinity::Text
2233        );
2234        assert_eq!(TypeAffinity::from_type_name("CLOB"), TypeAffinity::Text);
2235    }
2236
2237    #[test]
2238    fn test_affinity_blob_keyword() {
2239        assert_eq!(TypeAffinity::from_type_name("BLOB"), TypeAffinity::Blob);
2240        assert_eq!(TypeAffinity::from_type_name("blob"), TypeAffinity::Blob);
2241    }
2242
2243    #[test]
2244    fn test_affinity_empty_type() {
2245        assert_eq!(TypeAffinity::from_type_name(""), TypeAffinity::Blob);
2246    }
2247
2248    #[test]
2249    fn test_affinity_real_keyword() {
2250        assert_eq!(TypeAffinity::from_type_name("REAL"), TypeAffinity::Real);
2251        assert_eq!(TypeAffinity::from_type_name("DOUBLE"), TypeAffinity::Real);
2252        assert_eq!(
2253            TypeAffinity::from_type_name("DOUBLE PRECISION"),
2254            TypeAffinity::Real
2255        );
2256        assert_eq!(TypeAffinity::from_type_name("FLOAT"), TypeAffinity::Real);
2257    }
2258
2259    #[test]
2260    fn test_affinity_numeric_keyword() {
2261        assert_eq!(
2262            TypeAffinity::from_type_name("NUMERIC"),
2263            TypeAffinity::Numeric
2264        );
2265        assert_eq!(
2266            TypeAffinity::from_type_name("DECIMAL(10,5)"),
2267            TypeAffinity::Numeric
2268        );
2269        assert_eq!(
2270            TypeAffinity::from_type_name("BOOLEAN"),
2271            TypeAffinity::Numeric
2272        );
2273        assert_eq!(TypeAffinity::from_type_name("DATE"), TypeAffinity::Numeric);
2274        assert_eq!(
2275            TypeAffinity::from_type_name("DATETIME"),
2276            TypeAffinity::Numeric
2277        );
2278    }
2279
2280    #[test]
2281    fn test_affinity_case_insensitive() {
2282        assert_eq!(
2283            TypeAffinity::from_type_name("integer"),
2284            TypeAffinity::Integer
2285        );
2286        assert_eq!(TypeAffinity::from_type_name("text"), TypeAffinity::Text);
2287        assert_eq!(TypeAffinity::from_type_name("Real"), TypeAffinity::Real);
2288        assert_eq!(
2289            TypeAffinity::from_type_name("Numeric"),
2290            TypeAffinity::Numeric
2291        );
2292    }
2293
2294    #[test]
2295    fn test_affinity_first_match_int_before_char() {
2296        // "CHARINT" contains both "CHAR" and "INT", but "INT" is checked first.
2297        assert_eq!(
2298            TypeAffinity::from_type_name("CHARINT"),
2299            TypeAffinity::Integer
2300        );
2301        // "POINTERFLOAT" contains "INT" so INTEGER wins over REAL.
2302        assert_eq!(
2303            TypeAffinity::from_type_name("POINTERFLOAT"),
2304            TypeAffinity::Integer
2305        );
2306    }
2307
2308    #[test]
2309    fn test_comparison_numeric_vs_text() {
2310        assert_eq!(
2311            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Text),
2312            Some(TypeAffinity::Numeric)
2313        );
2314        assert_eq!(
2315            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Real),
2316            Some(TypeAffinity::Numeric)
2317        );
2318        assert_eq!(
2319            TypeAffinity::comparison_affinity(TypeAffinity::Numeric, TypeAffinity::Blob),
2320            Some(TypeAffinity::Numeric)
2321        );
2322    }
2323
2324    #[test]
2325    fn test_comparison_text_vs_blob() {
2326        assert_eq!(
2327            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Blob),
2328            Some(TypeAffinity::Text)
2329        );
2330        assert_eq!(
2331            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Text),
2332            Some(TypeAffinity::Text)
2333        );
2334    }
2335
2336    #[test]
2337    fn test_comparison_same_affinity_no_coercion() {
2338        assert_eq!(
2339            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Integer),
2340            None
2341        );
2342        assert_eq!(
2343            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Text),
2344            None
2345        );
2346        assert_eq!(
2347            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2348            None
2349        );
2350    }
2351
2352    #[test]
2353    fn test_comparison_both_blob_no_coercion() {
2354        assert_eq!(
2355            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2356            None
2357        );
2358    }
2359
2360    #[test]
2361    fn test_affinity_applied_to_needing_operand_only() {
2362        let left = SqliteValue::Integer(42);
2363        let right = SqliteValue::Text(Arc::from("123"));
2364        let affinity = TypeAffinity::comparison_affinity(left.affinity(), right.affinity())
2365            .expect("numeric-vs-text comparison must request numeric coercion");
2366
2367        // Numeric side should remain unchanged.
2368        let left_after = left.clone();
2369        // Text side is the side that needs conversion for numeric comparison.
2370        let right_after = right.apply_affinity(affinity);
2371
2372        assert_eq!(left_after, left);
2373        assert_eq!(right_after, SqliteValue::Integer(123));
2374    }
2375
2376    #[test]
2377    fn test_comparison_numeric_subtypes() {
2378        // INTEGER vs REAL: both numeric, different variants but no coercion needed
2379        // per SQLite rules (they share the numeric class).
2380        assert_eq!(
2381            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Real),
2382            None
2383        );
2384        assert_eq!(
2385            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Numeric),
2386            None
2387        );
2388        assert_eq!(
2389            TypeAffinity::comparison_affinity(TypeAffinity::Real, TypeAffinity::Numeric),
2390            None
2391        );
2392    }
2393
2394    // ── 5A.1: BTreePageHeader::write_empty_leaf_table tests (bd-2yy6) ──
2395
2396    #[test]
2397    fn test_write_empty_leaf_table_basic() {
2398        let ps = PageSize::DEFAULT;
2399        let mut buf = vec![0u8; ps.as_usize()];
2400        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2401
2402        assert_eq!(buf[0], 0x0D, "page type LeafTable");
2403        assert_eq!(buf[1], 0, "first_freeblock hi");
2404        assert_eq!(buf[2], 0, "first_freeblock lo");
2405        assert_eq!(buf[3], 0, "cell_count hi");
2406        assert_eq!(buf[4], 0, "cell_count lo");
2407        // 4096 = 0x1000
2408        assert_eq!(buf[5], 0x10, "content_offset hi");
2409        assert_eq!(buf[6], 0x00, "content_offset lo");
2410        assert_eq!(buf[7], 0, "fragmented_free_bytes");
2411    }
2412
2413    #[test]
2414    fn test_write_empty_leaf_table_page1_offset() {
2415        let ps = PageSize::DEFAULT;
2416        let mut buf = vec![0u8; ps.as_usize()];
2417        BTreePageHeader::write_empty_leaf_table(&mut buf, DATABASE_HEADER_SIZE, ps.get());
2418
2419        assert_eq!(buf[DATABASE_HEADER_SIZE], 0x0D, "page type at offset 100");
2420        // Bytes before offset 100 should be untouched.
2421        assert!(buf[..DATABASE_HEADER_SIZE].iter().all(|&b| b == 0));
2422    }
2423
2424    #[test]
2425    fn test_write_empty_leaf_table_65536_encoding() {
2426        let ps = PageSize::new(65536).unwrap();
2427        let mut buf = vec![0u8; ps.as_usize()];
2428        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2429
2430        // 65536 is encoded as 0 in the B-tree header.
2431        assert_eq!(buf[5], 0x00, "65536 encoded as 0 hi");
2432        assert_eq!(buf[6], 0x00, "65536 encoded as 0 lo");
2433    }
2434
2435    #[test]
2436    fn test_write_empty_leaf_table_512_page_size() {
2437        let ps = PageSize::new(512).unwrap();
2438        let mut buf = vec![0u8; ps.as_usize()];
2439        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2440
2441        // 512 = 0x0200
2442        assert_eq!(buf[5], 0x02, "512 hi byte");
2443        assert_eq!(buf[6], 0x00, "512 lo byte");
2444    }
2445}