Skip to main content

fsqlite_types/
lib.rs

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