1#![feature(portable_simd)]
2
3pub mod cx;
4pub mod ecs;
5pub mod encoding;
6pub mod eprocess;
7pub mod flags;
8pub mod glossary;
9pub mod limits;
10pub mod obligation;
11pub mod opcode;
12pub mod qsbr;
13pub mod record;
14pub mod record_coder_pacbayes;
15pub mod serial_type;
16pub mod sync_primitives;
17pub mod value;
18
19pub use cx::Cx;
20pub use ecs::{
21 ObjectId, PayloadHash, SYMBOL_RECORD_MAGIC, SYMBOL_RECORD_VERSION, SymbolReadPath,
22 SymbolRecord, SymbolRecordError, SymbolRecordFlags, SystematicLayoutError,
23 layout_systematic_run, reconstruct_systematic_happy_path, recover_object_with_fallback,
24 source_symbol_count, validate_systematic_run,
25};
26pub use eprocess::{
27 EProcessConfig, EProcessDecision, EProcessOracle, EProcessSignal, EProcessSnapshot,
28 EProcessTelemetryBridge,
29};
30pub use glossary::{
31 ArcCache, BtreeRef, Budget, COMMIT_MARKER_RECORD_V1_SIZE, ColumnIdx, CommitCapsule,
32 CommitMarker, CommitProof, CommitSeq, DecodeProof, DependencyEdge, EpochId, IdempotencyKey,
33 IndexId, IntentFootprint, IntentLog, IntentOp, IntentOpKind, OTI_WIRE_SIZE, OperatingMode, Oti,
34 Outcome, PageHistory, PageVersion, RangeKey, ReadWitness, RebaseBinaryOp, RebaseExpr,
35 RebaseUnaryOp, Region, RemoteCap, RootManifest, RowId, RowIdAllocator, RowIdExhausted,
36 RowIdMode, Saga, SchemaEpoch, SemanticKeyKind, SemanticKeyRef, Snapshot, StructuralEffects,
37 SymbolAuthMasterKeyCap, SymbolValidityWindow, TableId, TxnEpoch, TxnId, TxnSlot, TxnToken,
38 VersionPointer, WitnessIndexSegment, WitnessKey, WriteWitness,
39};
40pub use value::{SmallText, SqliteValue};
41
42use std::fmt;
43use std::num::NonZeroU32;
44use std::sync::atomic::{AtomicU64, Ordering};
45use std::sync::{Arc, OnceLock};
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
52#[repr(transparent)]
53pub struct PageNumber(NonZeroU32);
54
55impl PageNumber {
56 pub const ONE: Self = Self(NonZeroU32::MIN);
59
60 #[inline]
65 pub const fn new(n: u32) -> Option<Self> {
66 if n == u32::MAX {
67 None
68 } else {
69 match NonZeroU32::new(n) {
70 Some(v) => Some(Self(v)),
71 None => None,
72 }
73 }
74 }
75
76 #[inline]
78 pub const fn get(self) -> u32 {
79 self.0.get()
80 }
81}
82
83impl fmt::Display for PageNumber {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "{}", self.0)
86 }
87}
88
89impl serde::Serialize for PageNumber {
90 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
91 where
92 S: serde::Serializer,
93 {
94 serializer.serialize_u32(self.get())
95 }
96}
97
98impl<'de> serde::Deserialize<'de> for PageNumber {
99 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
100 where
101 D: serde::Deserializer<'de>,
102 {
103 let raw = <u32 as serde::Deserialize>::deserialize(deserializer)?;
104 Self::new(raw).ok_or_else(|| {
105 serde::de::Error::invalid_value(
106 serde::de::Unexpected::Unsigned(u64::from(raw)),
107 &"a SQLite page number in 1..=4294967294",
108 )
109 })
110 }
111}
112
113impl TryFrom<u32> for PageNumber {
114 type Error = InvalidPageNumber;
115
116 fn try_from(value: u32) -> Result<Self, Self::Error> {
117 Self::new(value).ok_or(InvalidPageNumber)
118 }
119}
120
121#[derive(Default)]
126pub struct PageNumberHasher(u64);
127
128impl std::hash::Hasher for PageNumberHasher {
129 fn write(&mut self, _: &[u8]) {
130 debug_assert!(false, "PageNumberHasher only supports write_u32");
133 }
134
135 fn write_u32(&mut self, n: u32) {
136 self.0 = u64::from(n);
137 }
138
139 fn finish(&self) -> u64 {
140 self.0
141 }
142}
143
144pub type PageNumberBuildHasher = std::hash::BuildHasherDefault<PageNumberHasher>;
146
147#[must_use]
149pub const fn gf256_add_byte(lhs: u8, rhs: u8) -> u8 {
150 lhs ^ rhs
151}
152
153#[must_use]
158pub fn gf256_mul_byte(mut a: u8, mut b: u8) -> u8 {
159 let mut out = 0_u8;
160 while b != 0 {
161 if (b & 1) != 0 {
162 out ^= a;
163 }
164 let carry = (a & 0x80) != 0;
165 a <<= 1;
166 if carry {
167 a ^= 0x1D;
168 }
169 b >>= 1;
170 }
171 out
172}
173
174#[must_use]
176pub fn gf256_inverse_byte(value: u8) -> Option<u8> {
177 if value == 0 {
178 return None;
179 }
180 for candidate in 1u16..=255 {
181 let inv = u8::try_from(candidate).expect("candidate in 1..=255 always fits u8");
182 if gf256_mul_byte(value, inv) == 1 {
183 return Some(inv);
184 }
185 }
186 None
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
191pub enum MergePageKind {
192 BtreeInteriorTable,
194 BtreeLeafTable,
196 BtreeInteriorIndex,
198 BtreeLeafIndex,
200 Overflow,
202 Freelist,
204 PointerMap,
206 Opaque,
208}
209
210impl MergePageKind {
211 #[must_use]
213 pub const fn is_sqlite_structured(self) -> bool {
214 !matches!(self, Self::Opaque)
215 }
216
217 #[must_use]
219 pub fn classify(page: &[u8]) -> Self {
220 let Some(first_byte) = page.first().copied() else {
221 return Self::Opaque;
222 };
223 match BTreePageType::from_byte(first_byte) {
224 Some(BTreePageType::LeafTable) => Self::BtreeLeafTable,
225 Some(BTreePageType::InteriorTable) => Self::BtreeInteriorTable,
226 Some(BTreePageType::LeafIndex) => Self::BtreeLeafIndex,
227 Some(BTreePageType::InteriorIndex) => Self::BtreeInteriorIndex,
228 None => Self::Opaque,
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub struct InvalidPageNumber;
236
237impl fmt::Display for InvalidPageNumber {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 f.write_str("page number must be in 1..=4294967294")
240 }
241}
242
243impl std::error::Error for InvalidPageNumber {}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250pub struct PageSize(u32);
251
252impl PageSize {
253 pub const MIN: Self = Self(512);
255
256 pub const DEFAULT: Self = Self(limits::DEFAULT_PAGE_SIZE);
258
259 pub const MAX: Self = Self(limits::MAX_PAGE_SIZE);
261
262 pub const fn new(size: u32) -> Option<Self> {
265 if size < 512 || size > 65536 || !size.is_power_of_two() {
266 None
267 } else {
268 Some(Self(size))
269 }
270 }
271
272 #[inline]
274 pub const fn get(self) -> u32 {
275 self.0
276 }
277
278 #[inline]
280 pub const fn as_usize(self) -> usize {
281 self.0 as usize
282 }
283
284 #[inline]
289 pub const fn usable(self, reserved: u8) -> u32 {
290 self.0 - reserved as u32
291 }
292}
293
294impl Default for PageSize {
295 fn default() -> Self {
296 Self::DEFAULT
297 }
298}
299
300impl fmt::Display for PageSize {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 write!(f, "{}", self.0)
303 }
304}
305
306pub struct PageData {
312 repr: PageDataRepr,
313 image_token: u64,
314}
315
316enum PageDataRepr {
317 Owned {
323 bytes: Vec<u8>,
324 shared: OnceLock<Arc<[u8]>>,
325 },
326 Shared(Arc<[u8]>),
328}
329
330impl Clone for PageData {
331 fn clone(&self) -> Self {
332 match &self.repr {
333 PageDataRepr::Owned { bytes, shared } => {
334 let shared = Arc::clone(
335 shared.get_or_init(|| Arc::<[u8]>::from(bytes.clone().into_boxed_slice())),
336 );
337 Self {
338 repr: PageDataRepr::Shared(shared),
339 image_token: self.image_token,
340 }
341 }
342 PageDataRepr::Shared(bytes) => Self {
343 repr: PageDataRepr::Shared(Arc::clone(bytes)),
344 image_token: self.image_token,
345 },
346 }
347 }
348}
349
350impl PartialEq for PageData {
351 fn eq(&self, other: &Self) -> bool {
352 self.as_bytes() == other.as_bytes()
353 }
354}
355
356impl Eq for PageData {}
357
358impl PageDataRepr {
359 #[inline]
360 fn as_bytes(&self) -> &[u8] {
361 match self {
362 Self::Owned { bytes, .. } => bytes.as_slice(),
363 Self::Shared(bytes) => bytes.as_ref(),
364 }
365 }
366}
367
368impl PageData {
369 fn next_image_token() -> u64 {
370 static NEXT_IMAGE_TOKEN: AtomicU64 = AtomicU64::new(1);
371 NEXT_IMAGE_TOKEN.fetch_add(1, Ordering::Relaxed).max(1)
372 }
373
374 fn bump_image_token(&mut self) {
375 self.image_token = Self::next_image_token();
376 }
377
378 fn invalidate_owned_snapshot_cache_if_needed(&mut self) {
379 let reset_owned_snapshot_cache = matches!(
380 &self.repr,
381 PageDataRepr::Owned { shared, .. } if shared.get().is_some()
382 );
383 if reset_owned_snapshot_cache {
384 let bytes = match std::mem::replace(
385 &mut self.repr,
386 PageDataRepr::Owned {
387 bytes: Vec::new(),
388 shared: OnceLock::new(),
389 },
390 ) {
391 PageDataRepr::Owned { bytes, .. } => bytes,
392 PageDataRepr::Shared(_) => {
393 unreachable!("owned snapshot cache reset should only run for owned pages")
394 }
395 };
396 self.repr = PageDataRepr::Owned {
397 bytes,
398 shared: OnceLock::new(),
399 };
400 }
401 }
402
403 pub fn zeroed(size: PageSize) -> Self {
405 Self::from_vec(vec![0u8; size.as_usize()])
406 }
407
408 pub fn from_vec(data: Vec<u8>) -> Self {
411 Self {
412 repr: PageDataRepr::Owned {
413 bytes: data,
414 shared: OnceLock::new(),
415 },
416 image_token: Self::next_image_token(),
417 }
418 }
419
420 #[must_use]
422 pub fn from_shared(bytes: Arc<[u8]>) -> Self {
423 Self {
424 repr: PageDataRepr::Shared(bytes),
425 image_token: Self::next_image_token(),
426 }
427 }
428
429 #[inline]
431 pub fn as_bytes(&self) -> &[u8] {
432 self.repr.as_bytes()
433 }
434
435 #[inline]
442 #[must_use]
443 pub fn image_token(&self) -> u64 {
444 self.image_token
445 }
446
447 #[inline]
451 pub fn as_bytes_mut(&mut self) -> &mut [u8] {
452 self.invalidate_owned_snapshot_cache_if_needed();
453 self.bump_image_token();
454 match &mut self.repr {
455 PageDataRepr::Owned { bytes, .. } => bytes.as_mut_slice(),
456 PageDataRepr::Shared(bytes) => Arc::make_mut(bytes),
457 }
458 }
459
460 #[inline]
468 #[must_use]
469 pub fn is_single_owner_owned(&self) -> bool {
470 matches!(
471 &self.repr,
472 PageDataRepr::Owned { shared, .. } if shared.get().is_none()
473 )
474 }
475
476 pub fn try_zero_extend_owned_to(&mut self, new_len: usize) -> bool {
481 self.invalidate_owned_snapshot_cache_if_needed();
482 match &mut self.repr {
483 PageDataRepr::Owned { bytes, .. } => {
484 if bytes.len() > new_len {
485 return false;
486 }
487 if bytes.len() < new_len {
488 self.image_token = Self::next_image_token();
489 bytes.resize(new_len, 0);
490 }
491 true
492 }
493 PageDataRepr::Shared(_) => false,
494 }
495 }
496
497 #[inline]
499 pub fn len(&self) -> usize {
500 self.as_bytes().len()
501 }
502
503 #[inline]
505 pub fn is_empty(&self) -> bool {
506 self.as_bytes().is_empty()
507 }
508
509 pub fn into_vec(self) -> Vec<u8> {
513 match self.repr {
514 PageDataRepr::Owned { bytes, .. } => bytes,
515 PageDataRepr::Shared(bytes) => bytes.as_ref().to_vec(),
516 }
517 }
518}
519
520impl fmt::Debug for PageData {
521 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522 f.debug_struct("PageData")
523 .field("len", &self.len())
524 .finish()
525 }
526}
527
528impl AsRef<[u8]> for PageData {
529 fn as_ref(&self) -> &[u8] {
530 self.as_bytes()
531 }
532}
533
534impl AsMut<[u8]> for PageData {
535 fn as_mut(&mut self) -> &mut [u8] {
536 self.as_bytes_mut()
537 }
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544#[repr(u8)]
545pub enum TypeAffinity {
546 Integer = b'D',
548 Text = b'B',
550 Blob = b'A',
552 Real = b'E',
554 Numeric = b'C',
557}
558
559impl TypeAffinity {
560 pub fn from_type_name(type_name: &str) -> Self {
569 let upper = type_name.to_ascii_uppercase();
570
571 if upper.contains("INT") {
572 Self::Integer
573 } else if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
574 Self::Text
575 } else if upper.is_empty() || upper.contains("BLOB") {
576 Self::Blob
577 } else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
578 Self::Real
579 } else {
580 Self::Numeric
581 }
582 }
583
584 pub fn comparison_affinity(left: Self, right: Self) -> Option<Self> {
597 if left == right {
598 return None;
599 }
600
601 let is_numeric = |a: Self| matches!(a, Self::Integer | Self::Real | Self::Numeric);
602
603 if is_numeric(left) && matches!(right, Self::Text | Self::Blob) {
605 return Some(Self::Numeric);
606 }
607 if is_numeric(right) && matches!(left, Self::Text | Self::Blob) {
608 return Some(Self::Numeric);
609 }
610
611 if (left == Self::Text && right == Self::Blob)
613 || (left == Self::Blob && right == Self::Text)
614 {
615 return Some(Self::Text);
616 }
617
618 None
620 }
621}
622
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
628#[repr(u8)]
629pub enum StorageClass {
630 Null = 1,
632 Integer = 2,
634 Real = 3,
636 Text = 4,
638 Blob = 5,
640}
641
642impl fmt::Display for StorageClass {
643 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
644 match self {
645 Self::Null => f.write_str("NULL"),
646 Self::Integer => f.write_str("INTEGER"),
647 Self::Real => f.write_str("REAL"),
648 Self::Text => f.write_str("TEXT"),
649 Self::Blob => f.write_str("BLOB"),
650 }
651 }
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
659pub enum StrictColumnType {
660 Integer,
662 Real,
664 Text,
666 Blob,
668 Any,
670}
671
672impl StrictColumnType {
673 pub fn from_type_name(name: &str) -> Option<Self> {
678 match name.to_ascii_uppercase().as_str() {
679 "INT" | "INTEGER" => Some(Self::Integer),
680 "REAL" => Some(Self::Real),
681 "TEXT" => Some(Self::Text),
682 "BLOB" => Some(Self::Blob),
683 "ANY" => Some(Self::Any),
684 _ => None,
685 }
686 }
687}
688
689#[derive(Debug, Clone, PartialEq, Eq)]
691pub struct StrictTypeError {
692 pub expected: StrictColumnType,
694 pub actual: StorageClass,
696}
697
698impl fmt::Display for StrictTypeError {
699 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700 write!(
701 f,
702 "cannot store {} value in {:?} column",
703 self.actual, self.expected
704 )
705 }
706}
707
708#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
710#[repr(u8)]
711pub enum TextEncoding {
712 #[default]
714 Utf8 = 1,
715 Utf16le = 2,
717 Utf16be = 3,
719}
720
721#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
723pub enum JournalMode {
724 #[default]
726 Delete,
727 Truncate,
729 Persist,
731 Memory,
733 Wal,
735 Off,
737}
738
739#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
741#[repr(u8)]
742pub enum SynchronousMode {
743 Off = 0,
745 Normal = 1,
747 #[default]
749 Full = 2,
750 Extra = 3,
752}
753
754#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
756#[repr(u8)]
757pub enum LockLevel {
758 #[default]
760 None = 0,
761 Shared = 1,
763 Reserved = 2,
765 Pending = 3,
767 Exclusive = 4,
769}
770
771#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
773#[repr(u8)]
774pub enum CheckpointMode {
775 Passive = 0,
777 Full = 1,
779 Restart = 2,
781 Truncate = 3,
783}
784
785#[derive(Debug, Clone, PartialEq, Eq)]
790pub struct DatabaseHeader {
791 pub page_size: PageSize,
793 pub write_version: u8,
795 pub read_version: u8,
797 pub reserved_per_page: u8,
799 pub change_counter: u32,
801 pub page_count: u32,
803 pub freelist_trunk: u32,
805 pub freelist_count: u32,
807 pub schema_cookie: u32,
809 pub schema_format: u32,
811 pub default_cache_size: i32,
813 pub largest_root_page: u32,
815 pub text_encoding: TextEncoding,
817 pub user_version: u32,
819 pub incremental_vacuum: u32,
821 pub application_id: u32,
823 pub version_valid_for: u32,
826 pub sqlite_version: u32,
828}
829
830impl Default for DatabaseHeader {
831 fn default() -> Self {
832 Self {
833 page_size: PageSize::DEFAULT,
834 write_version: 1,
835 read_version: 1,
836 reserved_per_page: 0,
837 change_counter: 0,
838 page_count: 0,
839 freelist_trunk: 0,
840 freelist_count: 0,
841 schema_cookie: 0,
842 schema_format: 4,
843 default_cache_size: -2000,
844 largest_root_page: 0,
845 text_encoding: TextEncoding::Utf8,
846 user_version: 0,
847 incremental_vacuum: 0,
848 application_id: 0,
849 version_valid_for: 0,
850 sqlite_version: 0,
851 }
852 }
853}
854
855pub const DATABASE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
857
858pub const DATABASE_HEADER_SIZE: usize = 100;
860
861pub const MAX_FILE_FORMAT_VERSION: u8 = 2;
867
868pub const FRANKENSQLITE_SQLITE_VERSION_NUMBER: u32 = 3_052_000;
872
873pub const FRANKENSQLITE_SQLITE_VERSION: &str = "3.52.0";
879
880pub const FRANKENSQLITE_SOURCE_ID: &str = "FrankenSQLite 0.1.0 (compatible with SQLite 3.52.0)";
882
883#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
885pub enum DatabaseOpenMode {
886 ReadWrite,
888 ReadOnly,
890}
891
892#[derive(Debug, Clone, PartialEq, Eq)]
894pub enum DatabaseHeaderError {
895 InvalidMagic,
897 InvalidPageSize { raw: u16 },
899 InvalidPayloadFractions { max: u8, min: u8, leaf: u8 },
901 UsableSizeTooSmall {
903 page_size: u32,
904 reserved_per_page: u8,
905 usable_size: u32,
906 },
907 UnsupportedReadVersion { read_version: u8, max_supported: u8 },
909 InvalidTextEncoding { raw: u32 },
911 InvalidSchemaFormat { raw: u32 },
913}
914
915impl fmt::Display for DatabaseHeaderError {
916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917 match self {
918 Self::InvalidMagic => f.write_str("invalid database header magic"),
919 Self::InvalidPageSize { raw } => write!(f, "invalid page size encoding: {raw}"),
920 Self::InvalidPayloadFractions { max, min, leaf } => write!(
921 f,
922 "invalid payload fractions: max={max} min={min} leaf={leaf}"
923 ),
924 Self::UsableSizeTooSmall {
925 page_size,
926 reserved_per_page,
927 usable_size,
928 } => write!(
929 f,
930 "usable page size too small: page_size={page_size} reserved={reserved_per_page} usable={usable_size}"
931 ),
932 Self::UnsupportedReadVersion {
933 read_version,
934 max_supported,
935 } => write!(
936 f,
937 "unsupported read format version: read_version={read_version} max_supported={max_supported}"
938 ),
939 Self::InvalidTextEncoding { raw } => write!(f, "invalid text encoding: {raw}"),
940 Self::InvalidSchemaFormat { raw } => write!(f, "invalid schema format: {raw}"),
941 }
942 }
943}
944
945impl std::error::Error for DatabaseHeaderError {}
946
947impl DatabaseHeader {
948 pub fn from_bytes(buf: &[u8; DATABASE_HEADER_SIZE]) -> Result<Self, DatabaseHeaderError> {
950 if &buf[..DATABASE_HEADER_MAGIC.len()] != DATABASE_HEADER_MAGIC {
951 return Err(DatabaseHeaderError::InvalidMagic);
952 }
953
954 let page_size_raw = encoding::read_u16_be(&buf[16..18]).expect("fixed u16 field");
955 let page_size_u32 = match page_size_raw {
956 1 => 65_536,
957 0 => return Err(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw }),
958 n => u32::from(n),
959 };
960 let page_size = PageSize::new(page_size_u32)
961 .ok_or(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw })?;
962
963 let write_version = buf[18];
964 let read_version = buf[19];
965 let reserved_per_page = buf[20];
966
967 let max_payload = buf[21];
968 let min_payload = buf[22];
969 let leaf_payload = buf[23];
970 if (max_payload, min_payload, leaf_payload) != (64, 32, 32) {
971 return Err(DatabaseHeaderError::InvalidPayloadFractions {
972 max: max_payload,
973 min: min_payload,
974 leaf: leaf_payload,
975 });
976 }
977
978 let usable_size = page_size.usable(reserved_per_page);
979 if usable_size < 480 {
980 return Err(DatabaseHeaderError::UsableSizeTooSmall {
981 page_size: page_size.get(),
982 reserved_per_page,
983 usable_size,
984 });
985 }
986
987 if read_version > MAX_FILE_FORMAT_VERSION {
989 return Err(DatabaseHeaderError::UnsupportedReadVersion {
990 read_version,
991 max_supported: MAX_FILE_FORMAT_VERSION,
992 });
993 }
994
995 let change_counter = encoding::read_u32_be(&buf[24..28]).expect("fixed u32 field");
996 let page_count = encoding::read_u32_be(&buf[28..32]).expect("fixed u32 field");
997 let freelist_trunk = encoding::read_u32_be(&buf[32..36]).expect("fixed u32 field");
998 let freelist_count = encoding::read_u32_be(&buf[36..40]).expect("fixed u32 field");
999 let schema_cookie = encoding::read_u32_be(&buf[40..44]).expect("fixed u32 field");
1000 let schema_format = encoding::read_u32_be(&buf[44..48]).expect("fixed u32 field");
1001
1002 if schema_format != 4 {
1005 return Err(DatabaseHeaderError::InvalidSchemaFormat { raw: schema_format });
1006 }
1007
1008 let default_cache_size = encoding::read_i32_be(&buf[48..52]).expect("fixed i32 field");
1009 let largest_root_page = encoding::read_u32_be(&buf[52..56]).expect("fixed u32 field");
1010
1011 let text_encoding_raw = encoding::read_u32_be(&buf[56..60]).expect("fixed u32 field");
1012 let text_encoding = match text_encoding_raw {
1013 1 => TextEncoding::Utf8,
1014 2 => TextEncoding::Utf16le,
1015 3 => TextEncoding::Utf16be,
1016 _ => {
1017 return Err(DatabaseHeaderError::InvalidTextEncoding {
1018 raw: text_encoding_raw,
1019 });
1020 }
1021 };
1022
1023 let user_version = encoding::read_u32_be(&buf[60..64]).expect("fixed u32 field");
1024 let incremental_vacuum = encoding::read_u32_be(&buf[64..68]).expect("fixed u32 field");
1025 let application_id = encoding::read_u32_be(&buf[68..72]).expect("fixed u32 field");
1026 let version_valid_for = encoding::read_u32_be(&buf[92..96]).expect("fixed u32 field");
1027 let sqlite_version = encoding::read_u32_be(&buf[96..100]).expect("fixed u32 field");
1028
1029 Ok(Self {
1030 page_size,
1031 write_version,
1032 read_version,
1033 reserved_per_page,
1034 change_counter,
1035 page_count,
1036 freelist_trunk,
1037 freelist_count,
1038 schema_cookie,
1039 schema_format,
1040 default_cache_size,
1041 largest_root_page,
1042 text_encoding,
1043 user_version,
1044 incremental_vacuum,
1045 application_id,
1046 version_valid_for,
1047 sqlite_version,
1048 })
1049 }
1050
1051 pub const fn open_mode(
1053 &self,
1054 max_supported: u8,
1055 ) -> Result<DatabaseOpenMode, DatabaseHeaderError> {
1056 if self.read_version > max_supported {
1057 return Err(DatabaseHeaderError::UnsupportedReadVersion {
1058 read_version: self.read_version,
1059 max_supported,
1060 });
1061 }
1062 if self.write_version > max_supported {
1063 return Ok(DatabaseOpenMode::ReadOnly);
1064 }
1065 Ok(DatabaseOpenMode::ReadWrite)
1066 }
1067
1068 pub const fn is_page_count_stale(&self) -> bool {
1075 self.version_valid_for != self.change_counter
1076 }
1077
1078 #[allow(clippy::cast_possible_truncation)]
1084 pub const fn page_count_from_file_size(&self, file_size: u64) -> Option<u32> {
1085 let ps = self.page_size.get() as u64;
1086 if file_size == 0 || file_size % ps != 0 {
1087 return None;
1088 }
1089 let count = file_size / ps;
1090 if count > u32::MAX as u64 {
1091 return None;
1092 }
1093 Some(count as u32)
1094 }
1095
1096 pub fn write_to_bytes(
1098 &self,
1099 out: &mut [u8; DATABASE_HEADER_SIZE],
1100 ) -> Result<(), DatabaseHeaderError> {
1101 if self.schema_format != 4 {
1103 return Err(DatabaseHeaderError::InvalidSchemaFormat {
1104 raw: self.schema_format,
1105 });
1106 }
1107
1108 let usable_size = self.page_size.usable(self.reserved_per_page);
1109 if usable_size < 480 {
1110 return Err(DatabaseHeaderError::UsableSizeTooSmall {
1111 page_size: self.page_size.get(),
1112 reserved_per_page: self.reserved_per_page,
1113 usable_size,
1114 });
1115 }
1116
1117 out.fill(0);
1118 out[..DATABASE_HEADER_MAGIC.len()].copy_from_slice(DATABASE_HEADER_MAGIC);
1119
1120 let page_size_raw = if self.page_size.get() == 65_536 {
1122 1u16
1123 } else {
1124 #[allow(clippy::cast_possible_truncation)]
1125 {
1126 self.page_size.get() as u16
1127 }
1128 };
1129 encoding::write_u16_be(&mut out[16..18], page_size_raw).expect("fixed u16 field");
1130
1131 out[18] = self.write_version;
1132 out[19] = self.read_version;
1133 out[20] = self.reserved_per_page;
1134
1135 out[21] = 64;
1137 out[22] = 32;
1138 out[23] = 32;
1139
1140 encoding::write_u32_be(&mut out[24..28], self.change_counter).expect("fixed u32 field");
1141 encoding::write_u32_be(&mut out[28..32], self.page_count).expect("fixed u32 field");
1142 encoding::write_u32_be(&mut out[32..36], self.freelist_trunk).expect("fixed u32 field");
1143 encoding::write_u32_be(&mut out[36..40], self.freelist_count).expect("fixed u32 field");
1144 encoding::write_u32_be(&mut out[40..44], self.schema_cookie).expect("fixed u32 field");
1145 encoding::write_u32_be(&mut out[44..48], self.schema_format).expect("fixed u32 field");
1146 encoding::write_i32_be(&mut out[48..52], self.default_cache_size).expect("fixed i32 field");
1147 encoding::write_u32_be(&mut out[52..56], self.largest_root_page).expect("fixed u32 field");
1148
1149 let text_encoding_u32 = match self.text_encoding {
1150 TextEncoding::Utf8 => 1u32,
1151 TextEncoding::Utf16le => 2u32,
1152 TextEncoding::Utf16be => 3u32,
1153 };
1154 encoding::write_u32_be(&mut out[56..60], text_encoding_u32).expect("fixed u32 field");
1155
1156 encoding::write_u32_be(&mut out[60..64], self.user_version).expect("fixed u32 field");
1157 encoding::write_u32_be(&mut out[64..68], self.incremental_vacuum).expect("fixed u32 field");
1158 encoding::write_u32_be(&mut out[68..72], self.application_id).expect("fixed u32 field");
1159
1160 encoding::write_u32_be(&mut out[92..96], self.version_valid_for).expect("fixed u32 field");
1162 encoding::write_u32_be(&mut out[96..100], self.sqlite_version).expect("fixed u32 field");
1163
1164 Ok(())
1165 }
1166
1167 pub fn to_bytes(&self) -> Result<[u8; DATABASE_HEADER_SIZE], DatabaseHeaderError> {
1169 let mut out = [0u8; DATABASE_HEADER_SIZE];
1170 self.write_to_bytes(&mut out)?;
1171 Ok(out)
1172 }
1173}
1174
1175pub const BTREE_MAX_FRAGMENTED_FREE_BYTES: u8 = 60;
1177
1178#[derive(Debug, Clone, PartialEq, Eq)]
1180pub enum BTreePageError {
1181 PageSizeMismatch { expected: usize, actual: usize },
1183 PageTooSmall { usable_size: usize, needed: usize },
1185 InvalidPageType { raw: u8 },
1187 InvalidFragmentedFreeBytes { raw: u8, max: u8 },
1189 InvalidCellContentAreaStart {
1191 raw: u16,
1192 decoded: u32,
1193 usable_size: usize,
1194 },
1195 CellContentAreaOverlapsCellPointers {
1197 cell_content_start: u32,
1198 cell_pointer_array_end: usize,
1199 },
1200 CellPointerArrayOutOfBounds {
1202 start: usize,
1203 len: usize,
1204 usable_size: usize,
1205 },
1206 InvalidCellPointer {
1208 index: usize,
1209 offset: u16,
1210 usable_size: usize,
1211 },
1212 InvalidFreeblock {
1214 offset: u16,
1215 size: u16,
1216 usable_size: usize,
1217 },
1218 FreeblockLoop { offset: u16 },
1220 InvalidRightMostChild { raw: u32 },
1222}
1223
1224impl fmt::Display for BTreePageError {
1225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1226 match self {
1227 Self::PageSizeMismatch { expected, actual } => write!(
1228 f,
1229 "page size mismatch: expected {expected} bytes, got {actual} bytes"
1230 ),
1231 Self::PageTooSmall {
1232 usable_size,
1233 needed,
1234 } => write!(
1235 f,
1236 "page too small: usable_size={usable_size} needed={needed}"
1237 ),
1238 Self::InvalidPageType { raw } => write!(f, "invalid B-tree page type: {raw:#04x}"),
1239 Self::InvalidFragmentedFreeBytes { raw, max } => {
1240 write!(f, "invalid fragmented free bytes: {raw} (max {max})")
1241 }
1242 Self::InvalidCellContentAreaStart {
1243 raw,
1244 decoded,
1245 usable_size,
1246 } => write!(
1247 f,
1248 "invalid cell content area start: raw={raw} decoded={decoded} usable_size={usable_size}"
1249 ),
1250 Self::CellContentAreaOverlapsCellPointers {
1251 cell_content_start,
1252 cell_pointer_array_end,
1253 } => write!(
1254 f,
1255 "cell content area overlaps cell pointer array: cell_content_start={cell_content_start} cell_pointer_array_end={cell_pointer_array_end}"
1256 ),
1257 Self::CellPointerArrayOutOfBounds {
1258 start,
1259 len,
1260 usable_size,
1261 } => write!(
1262 f,
1263 "cell pointer array out of bounds: start={start} len={len} usable_size={usable_size}"
1264 ),
1265 Self::InvalidCellPointer {
1266 index,
1267 offset,
1268 usable_size,
1269 } => write!(
1270 f,
1271 "invalid cell pointer: index={index} offset={offset} usable_size={usable_size}"
1272 ),
1273 Self::InvalidFreeblock {
1274 offset,
1275 size,
1276 usable_size,
1277 } => write!(
1278 f,
1279 "invalid freeblock: offset={offset} size={size} usable_size={usable_size}"
1280 ),
1281 Self::FreeblockLoop { offset } => write!(f, "freeblock loop at offset {offset}"),
1282 Self::InvalidRightMostChild { raw } => {
1283 write!(f, "invalid right-most child pointer: {raw}")
1284 }
1285 }
1286 }
1287}
1288
1289impl std::error::Error for BTreePageError {}
1290
1291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1293pub struct BTreePageHeader {
1294 pub header_offset: usize,
1296 pub page_type: BTreePageType,
1298 pub first_freeblock: u16,
1300 pub cell_count: u16,
1302 pub cell_content_start: u32,
1304 pub fragmented_free_bytes: u8,
1306 pub right_most_child: Option<PageNumber>,
1308}
1309
1310impl BTreePageHeader {
1311 pub const fn header_size(self) -> usize {
1313 if self.page_type.is_leaf() { 8 } else { 12 }
1314 }
1315
1316 pub fn parse(
1318 page: &[u8],
1319 page_size: PageSize,
1320 reserved_per_page: u8,
1321 is_page1: bool,
1322 ) -> Result<Self, BTreePageError> {
1323 let expected = page_size.as_usize();
1324 if page.len() != expected {
1325 return Err(BTreePageError::PageSizeMismatch {
1326 expected,
1327 actual: page.len(),
1328 });
1329 }
1330
1331 let usable_size = page_size.usable(reserved_per_page) as usize;
1332 let header_offset = if is_page1 { DATABASE_HEADER_SIZE } else { 0 };
1333 let min_needed = header_offset + 8;
1334 if usable_size < min_needed {
1335 return Err(BTreePageError::PageTooSmall {
1336 usable_size,
1337 needed: min_needed,
1338 });
1339 }
1340
1341 let page_type_raw = page[header_offset];
1342 let page_type = BTreePageType::from_byte(page_type_raw)
1343 .ok_or(BTreePageError::InvalidPageType { raw: page_type_raw })?;
1344
1345 let header_size = if page_type.is_leaf() { 8 } else { 12 };
1346 let needed = header_offset + header_size;
1347 if usable_size < needed {
1348 return Err(BTreePageError::PageTooSmall {
1349 usable_size,
1350 needed,
1351 });
1352 }
1353
1354 let first_freeblock =
1355 u16::from_be_bytes([page[header_offset + 1], page[header_offset + 2]]);
1356 let cell_count = u16::from_be_bytes([page[header_offset + 3], page[header_offset + 4]]);
1357 let cell_content_raw =
1358 u16::from_be_bytes([page[header_offset + 5], page[header_offset + 6]]);
1359 let cell_content_start = if cell_content_raw == 0 {
1360 65_536
1361 } else {
1362 u32::from(cell_content_raw)
1363 };
1364 let usable_size_u32 = u32::try_from(usable_size).unwrap_or(u32::MAX);
1365 if cell_content_start > usable_size_u32 {
1366 return Err(BTreePageError::InvalidCellContentAreaStart {
1367 raw: cell_content_raw,
1368 decoded: cell_content_start,
1369 usable_size,
1370 });
1371 }
1372
1373 let fragmented_free_bytes = page[header_offset + 7];
1374 if fragmented_free_bytes > BTREE_MAX_FRAGMENTED_FREE_BYTES {
1375 return Err(BTreePageError::InvalidFragmentedFreeBytes {
1376 raw: fragmented_free_bytes,
1377 max: BTREE_MAX_FRAGMENTED_FREE_BYTES,
1378 });
1379 }
1380
1381 let right_most_child = if page_type.is_interior() {
1382 let raw = u32::from_be_bytes([
1383 page[header_offset + 8],
1384 page[header_offset + 9],
1385 page[header_offset + 10],
1386 page[header_offset + 11],
1387 ]);
1388 let pn = PageNumber::new(raw).ok_or(BTreePageError::InvalidRightMostChild { raw })?;
1389 Some(pn)
1390 } else {
1391 None
1392 };
1393
1394 let ptr_array_start = header_offset + header_size;
1396 let ptr_array_len = usize::from(cell_count) * 2;
1397 if ptr_array_start + ptr_array_len > usable_size {
1398 return Err(BTreePageError::CellPointerArrayOutOfBounds {
1399 start: ptr_array_start,
1400 len: ptr_array_len,
1401 usable_size,
1402 });
1403 }
1404 let ptr_array_end = ptr_array_start + ptr_array_len;
1405 let ptr_array_end_u32 = u32::try_from(ptr_array_end).unwrap_or(u32::MAX);
1406 if cell_content_start < ptr_array_end_u32 {
1407 return Err(BTreePageError::CellContentAreaOverlapsCellPointers {
1408 cell_content_start,
1409 cell_pointer_array_end: ptr_array_end,
1410 });
1411 }
1412
1413 Ok(Self {
1414 header_offset,
1415 page_type,
1416 first_freeblock,
1417 cell_count,
1418 cell_content_start,
1419 fragmented_free_bytes,
1420 right_most_child,
1421 })
1422 }
1423
1424 pub fn parse_cell_pointers(
1426 self,
1427 page: &[u8],
1428 page_size: PageSize,
1429 reserved_per_page: u8,
1430 ) -> Result<Vec<u16>, BTreePageError> {
1431 let expected = page_size.as_usize();
1432 if page.len() != expected {
1433 return Err(BTreePageError::PageSizeMismatch {
1434 expected,
1435 actual: page.len(),
1436 });
1437 }
1438
1439 let usable_size = page_size.usable(reserved_per_page) as usize;
1440 let ptr_array_start = self.header_offset + self.header_size();
1441 let ptr_array_len = usize::from(self.cell_count) * 2;
1442 if ptr_array_start + ptr_array_len > usable_size {
1443 return Err(BTreePageError::CellPointerArrayOutOfBounds {
1444 start: ptr_array_start,
1445 len: ptr_array_len,
1446 usable_size,
1447 });
1448 }
1449
1450 let min_cell_offset = ptr_array_start + ptr_array_len;
1451 let mut out = Vec::with_capacity(self.cell_count as usize);
1452 for i in 0..self.cell_count as usize {
1453 let off = ptr_array_start + i * 2;
1454 let cell_off = u16::from_be_bytes([page[off], page[off + 1]]);
1455 let cell_off_usize = usize::from(cell_off);
1456 if cell_off_usize < min_cell_offset
1457 || cell_off_usize < self.cell_content_start as usize
1458 || cell_off_usize >= usable_size
1459 {
1460 return Err(BTreePageError::InvalidCellPointer {
1461 index: i,
1462 offset: cell_off,
1463 usable_size,
1464 });
1465 }
1466 out.push(cell_off);
1467 }
1468 Ok(out)
1469 }
1470
1471 pub fn parse_freeblocks(
1473 self,
1474 page: &[u8],
1475 page_size: PageSize,
1476 reserved_per_page: u8,
1477 ) -> Result<Vec<Freeblock>, BTreePageError> {
1478 let expected = page_size.as_usize();
1479 if page.len() != expected {
1480 return Err(BTreePageError::PageSizeMismatch {
1481 expected,
1482 actual: page.len(),
1483 });
1484 }
1485 let usable_size = page_size.usable(reserved_per_page) as usize;
1486
1487 let mut blocks = Vec::new();
1488 let mut seen = std::collections::BTreeSet::new();
1489 let mut offset = self.first_freeblock;
1490 while offset != 0 {
1491 if !seen.insert(offset) {
1492 return Err(BTreePageError::FreeblockLoop { offset });
1493 }
1494
1495 let off = usize::from(offset);
1496 if off < self.cell_content_start as usize {
1497 return Err(BTreePageError::InvalidFreeblock {
1498 offset,
1499 size: 0,
1500 usable_size,
1501 });
1502 }
1503 if off + 4 > usable_size {
1504 return Err(BTreePageError::InvalidFreeblock {
1505 offset,
1506 size: 0,
1507 usable_size,
1508 });
1509 }
1510
1511 let next = u16::from_be_bytes([page[off], page[off + 1]]);
1512 let size = u16::from_be_bytes([page[off + 2], page[off + 3]]);
1513 if size < 4 || off + usize::from(size) > usable_size {
1514 return Err(BTreePageError::InvalidFreeblock {
1515 offset,
1516 size,
1517 usable_size,
1518 });
1519 }
1520
1521 blocks.push(Freeblock { offset, next, size });
1522 offset = next;
1523 }
1524
1525 Ok(blocks)
1526 }
1527
1528 #[allow(clippy::cast_possible_truncation)]
1542 pub fn write_empty_leaf_table(page: &mut [u8], header_offset: usize, usable_size: u32) {
1543 page[header_offset] = BTreePageType::LeafTable as u8; page[header_offset + 1] = 0;
1546 page[header_offset + 2] = 0;
1547 page[header_offset + 3] = 0;
1549 page[header_offset + 4] = 0;
1550 let content_raw = if usable_size >= 65_536 {
1552 0u16
1553 } else {
1554 usable_size as u16
1555 };
1556 page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1557 page[header_offset + 7] = 0;
1559 }
1560
1561 #[allow(clippy::cast_possible_truncation)]
1569 pub fn write_empty_leaf_index(page: &mut [u8], header_offset: usize, usable_size: u32) {
1570 page[header_offset] = BTreePageType::LeafIndex as u8; page[header_offset + 1] = 0;
1573 page[header_offset + 2] = 0;
1574 page[header_offset + 3] = 0;
1576 page[header_offset + 4] = 0;
1577 let content_raw = if usable_size >= 65_536 {
1579 0u16
1580 } else {
1581 usable_size as u16
1582 };
1583 page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1584 page[header_offset + 7] = 0;
1586 }
1587}
1588
1589#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1591pub struct Freeblock {
1592 pub offset: u16,
1593 pub next: u16,
1594 pub size: u16,
1595}
1596
1597pub const fn would_exceed_fragmented_free_bytes(current: u8, additional: u8) -> bool {
1599 current.saturating_add(additional) > BTREE_MAX_FRAGMENTED_FREE_BYTES
1600}
1601
1602#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1604#[repr(u8)]
1605pub enum BTreePageType {
1606 InteriorIndex = 2,
1608 InteriorTable = 5,
1610 LeafIndex = 10,
1612 LeafTable = 13,
1614}
1615
1616impl BTreePageType {
1617 pub const fn from_byte(b: u8) -> Option<Self> {
1619 match b {
1620 2 => Some(Self::InteriorIndex),
1621 5 => Some(Self::InteriorTable),
1622 10 => Some(Self::LeafIndex),
1623 13 => Some(Self::LeafTable),
1624 _ => None,
1625 }
1626 }
1627
1628 pub const fn is_leaf(self) -> bool {
1630 matches!(self, Self::LeafIndex | Self::LeafTable)
1631 }
1632
1633 pub const fn is_interior(self) -> bool {
1635 matches!(self, Self::InteriorIndex | Self::InteriorTable)
1636 }
1637
1638 pub const fn is_table(self) -> bool {
1640 matches!(self, Self::InteriorTable | Self::LeafTable)
1641 }
1642
1643 pub const fn is_index(self) -> bool {
1645 matches!(self, Self::InteriorIndex | Self::LeafIndex)
1646 }
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651 use super::*;
1652 use crate::value::SmallText;
1653
1654 #[test]
1655 fn page_number_zero_is_invalid() {
1656 assert!(PageNumber::new(0).is_none());
1657 assert!(PageNumber::try_from(0u32).is_err());
1658 }
1659
1660 #[test]
1661 fn test_page_number_zero_rejected() {
1662 assert!(PageNumber::new(0).is_none());
1663 assert!(PageNumber::try_from(0u32).is_err());
1664 }
1665
1666 #[test]
1667 fn page_number_max_u32_is_invalid() {
1668 assert!(PageNumber::new(u32::MAX).is_none());
1669 assert!(PageNumber::try_from(u32::MAX).is_err());
1670 assert_eq!(
1671 PageNumber::new(u32::MAX - 1)
1672 .expect("SQLite maximum page number should be valid")
1673 .get(),
1674 u32::MAX - 1
1675 );
1676 }
1677
1678 #[test]
1679 fn page_number_serde_preserves_constructor_invariant() {
1680 let max =
1681 PageNumber::new(u32::MAX - 1).expect("SQLite maximum page number should be valid");
1682 let encoded = serde_json::to_string(&max).expect("PageNumber should serialize as a u32");
1683 assert_eq!(encoded, (u32::MAX - 1).to_string());
1684 assert_eq!(
1685 serde_json::from_str::<PageNumber>(&encoded)
1686 .expect("valid serialized PageNumber should decode"),
1687 max
1688 );
1689
1690 let err = serde_json::from_str::<PageNumber>(&u32::MAX.to_string())
1691 .expect_err("serde must reject page numbers outside SQLite's valid range");
1692 assert!(
1693 err.to_string().contains("SQLite page number"),
1694 "unexpected serde error: {err}"
1695 );
1696 }
1697
1698 #[test]
1699 fn page_number_valid() {
1700 let pn = PageNumber::new(1).unwrap();
1701 assert_eq!(pn.get(), 1);
1702 assert_eq!(pn, PageNumber::ONE);
1703
1704 let pn = PageNumber::new(42).unwrap();
1705 assert_eq!(pn.get(), 42);
1706 assert_eq!(pn.to_string(), "42");
1707 }
1708
1709 #[test]
1710 fn page_number_ordering() {
1711 let a = PageNumber::new(1).unwrap();
1712 let b = PageNumber::new(100).unwrap();
1713 assert!(a < b);
1714 }
1715
1716 #[test]
1717 fn page_size_validation() {
1718 assert!(PageSize::new(0).is_none());
1719 assert!(PageSize::new(256).is_none());
1720 assert!(PageSize::new(511).is_none());
1721 assert!(PageSize::new(513).is_none());
1722 assert!(PageSize::new(1000).is_none());
1723 assert!(PageSize::new(131_072).is_none());
1724
1725 assert!(PageSize::new(512).is_some());
1726 assert!(PageSize::new(1024).is_some());
1727 assert!(PageSize::new(4096).is_some());
1728 assert!(PageSize::new(8192).is_some());
1729 assert!(PageSize::new(16384).is_some());
1730 assert!(PageSize::new(32768).is_some());
1731 assert!(PageSize::new(65536).is_some());
1732 }
1733
1734 #[test]
1735 fn page_size_defaults() {
1736 assert_eq!(PageSize::DEFAULT.get(), 4096);
1737 assert_eq!(PageSize::MIN.get(), 512);
1738 assert_eq!(PageSize::MAX.get(), 65536);
1739 assert_eq!(PageSize::default(), PageSize::DEFAULT);
1740 }
1741
1742 #[test]
1743 fn page_data_clone_promotes_owned_bytes_to_shared_snapshot() {
1744 let page = PageData::from_vec(vec![1, 2, 3, 4]);
1745 let PageDataRepr::Owned { shared, .. } = &page.repr else {
1746 panic!("fresh page data should start owned");
1747 };
1748 assert!(
1749 shared.get().is_none(),
1750 "fresh page should not allocate Arc eagerly"
1751 );
1752
1753 let cloned = page.clone();
1754
1755 let PageDataRepr::Owned { shared, .. } = &page.repr else {
1756 panic!("original page should remain in owned mode");
1757 };
1758 assert!(
1759 shared.get().is_some(),
1760 "first clone should materialize a shared snapshot lazily"
1761 );
1762 assert!(
1763 matches!(cloned.repr, PageDataRepr::Shared(_)),
1764 "clone should observe the shared snapshot"
1765 );
1766 }
1767
1768 #[test]
1769 fn page_data_mutation_reuses_owned_bytes_after_snapshot_clone() {
1770 let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1771 let snapshot = page.clone();
1772
1773 page.as_bytes_mut()[0] = 1;
1774
1775 assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
1776 assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
1777 assert!(
1778 matches!(page.repr, PageDataRepr::Owned { .. }),
1779 "mutating the original owner should stay on its owned bytes"
1780 );
1781 let PageDataRepr::Owned { shared, .. } = &page.repr else {
1782 panic!("mutated page should remain in owned mode");
1783 };
1784 assert!(
1785 shared.get().is_none(),
1786 "mutating the original owner must invalidate the stale shared snapshot cache so later clones observe the new bytes"
1787 );
1788 }
1789
1790 #[test]
1791 fn page_data_clone_after_owner_mutation_observes_latest_bytes() {
1792 let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1793 let first_snapshot = page.clone();
1794
1795 page.as_bytes_mut()[0] = 1;
1796 let second_snapshot = page.clone();
1797
1798 assert_eq!(first_snapshot.as_bytes(), &[9, 8, 7, 6]);
1799 assert_eq!(second_snapshot.as_bytes(), &[1, 8, 7, 6]);
1800 assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
1801 }
1802
1803 #[test]
1804 fn page_data_image_token_tracks_clone_and_mutation_boundaries() {
1805 let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1806 let original_token = page.image_token();
1807 let snapshot = page.clone();
1808
1809 assert_eq!(
1810 snapshot.image_token(),
1811 original_token,
1812 "immutable clones must share the same page-image token"
1813 );
1814
1815 page.as_bytes_mut()[0] = 1;
1816 assert_ne!(
1817 page.image_token(),
1818 original_token,
1819 "mutable access must move the owner to a fresh page-image token"
1820 );
1821 assert_eq!(
1822 snapshot.image_token(),
1823 original_token,
1824 "old snapshots retain the old image token"
1825 );
1826
1827 let second_snapshot = page.clone();
1828 assert_eq!(
1829 second_snapshot.image_token(),
1830 page.image_token(),
1831 "new snapshots observe the latest token"
1832 );
1833 }
1834
1835 #[test]
1836 fn page_data_try_zero_extend_owned_to_preserves_owned_bytes_and_invalidates_stale_snapshot() {
1837 let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1838 let snapshot = page.clone();
1839 let original_token = page.image_token();
1840
1841 assert!(page.try_zero_extend_owned_to(8));
1842 assert_eq!(page.as_bytes(), &[9, 8, 7, 6, 0, 0, 0, 0]);
1843 assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
1844 assert_ne!(
1845 page.image_token(),
1846 original_token,
1847 "zero extension mutates the page image and must bump the token"
1848 );
1849 assert!(
1850 matches!(page.repr, PageDataRepr::Owned { .. }),
1851 "zero-extending an owned page should stay on the owned representation"
1852 );
1853 let PageDataRepr::Owned { shared, .. } = &page.repr else {
1854 panic!("zero-extended page should remain owned");
1855 };
1856 assert!(
1857 shared.get().is_none(),
1858 "zero-extending must invalidate any stale shared snapshot cache"
1859 );
1860 }
1861
1862 #[test]
1863 fn page_data_try_zero_extend_owned_to_returns_false_for_shared_pages() {
1864 let original = PageData::from_vec(vec![1, 2, 3, 4]);
1865 let mut shared = original.clone();
1866
1867 assert!(!shared.try_zero_extend_owned_to(8));
1868 assert_eq!(shared.as_bytes(), &[1, 2, 3, 4]);
1869 }
1870
1871 fn make_header_for_tests() -> DatabaseHeader {
1872 DatabaseHeader {
1873 page_size: PageSize::DEFAULT,
1874 write_version: 2,
1875 read_version: 2,
1876 reserved_per_page: 0,
1877 change_counter: 7,
1878 page_count: 123,
1879 freelist_trunk: 0,
1880 freelist_count: 0,
1881 schema_cookie: 1,
1882 schema_format: 4,
1883 default_cache_size: -2000,
1884 largest_root_page: 0,
1885 text_encoding: TextEncoding::Utf8,
1886 user_version: 0,
1887 incremental_vacuum: 0,
1888 application_id: 0,
1889 version_valid_for: 7,
1890 sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
1891 }
1892 }
1893
1894 #[test]
1895 fn test_header_magic_validation() {
1896 let hdr = make_header_for_tests();
1897 let mut buf = hdr.to_bytes().unwrap();
1898 let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
1899 assert_eq!(parsed, hdr);
1900
1901 buf[0] = b'X';
1902 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1903 assert!(matches!(err, DatabaseHeaderError::InvalidMagic));
1904 }
1905
1906 #[test]
1907 fn test_header_page_size_encoding() {
1908 let mut hdr = make_header_for_tests();
1910 hdr.page_size = PageSize::new(65_536).unwrap();
1911 let buf = hdr.to_bytes().unwrap();
1912 assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), 1);
1913 assert_eq!(
1914 DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1915 65_536
1916 );
1917
1918 for size in [512u32, 1024, 2048, 4096, 8192, 16_384, 32_768] {
1920 hdr.page_size = PageSize::new(size).unwrap();
1921 let buf = hdr.to_bytes().unwrap();
1922 let expected_u16 = u16::try_from(size).unwrap();
1923 assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), expected_u16);
1924 assert_eq!(
1925 DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1926 size
1927 );
1928 }
1929
1930 let mut buf = make_header_for_tests().to_bytes().unwrap();
1932 buf[16..18].copy_from_slice(&1000u16.to_be_bytes());
1933 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1934 assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1935 }
1936
1937 #[test]
1938 fn test_header_page_size_range() {
1939 let mut buf = make_header_for_tests().to_bytes().unwrap();
1940 buf[16..18].copy_from_slice(&256u16.to_be_bytes());
1941 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1942 assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1943 }
1944
1945 #[test]
1946 fn test_header_write_read_version() {
1947 let mut hdr = make_header_for_tests();
1948
1949 hdr.write_version = 2;
1950 hdr.read_version = 2;
1951 assert_eq!(
1952 hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1953 DatabaseOpenMode::ReadWrite
1954 );
1955
1956 hdr.read_version = 3;
1957 let err = hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap_err();
1958 assert!(matches!(
1959 err,
1960 DatabaseHeaderError::UnsupportedReadVersion { .. }
1961 ));
1962
1963 hdr.read_version = 2;
1964 hdr.write_version = 3;
1965 assert_eq!(
1966 hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1967 DatabaseOpenMode::ReadOnly
1968 );
1969 }
1970
1971 #[test]
1972 fn test_header_payload_fractions() {
1973 let mut buf = make_header_for_tests().to_bytes().unwrap();
1974 buf[21] = 65;
1975 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1976 assert!(matches!(
1977 err,
1978 DatabaseHeaderError::InvalidPayloadFractions { .. }
1979 ));
1980 }
1981
1982 #[test]
1983 fn test_header_usable_size_minimum() {
1984 let mut buf = make_header_for_tests().to_bytes().unwrap();
1986 buf[16..18].copy_from_slice(&512u16.to_be_bytes());
1987 buf[20] = 33;
1988 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1989 assert!(matches!(
1990 err,
1991 DatabaseHeaderError::UsableSizeTooSmall { .. }
1992 ));
1993
1994 buf[20] = 32;
1995 DatabaseHeader::from_bytes(&buf).unwrap();
1996 }
1997
1998 #[test]
1999 fn test_header_round_trip() {
2000 let hdr = make_header_for_tests();
2001 let buf1 = hdr.to_bytes().unwrap();
2002 let parsed = DatabaseHeader::from_bytes(&buf1).unwrap();
2003 assert_eq!(parsed, hdr);
2004
2005 let buf2 = parsed.to_bytes().unwrap();
2006 assert_eq!(buf1, buf2);
2007 }
2008
2009 #[test]
2010 fn test_btree_page_header_leaf() {
2011 let page_size = PageSize::new(512).unwrap();
2012 let mut page = vec![0u8; page_size.as_usize()];
2013
2014 page[0] = 0x0D;
2016 page[1..3].copy_from_slice(&0u16.to_be_bytes()); page[3..5].copy_from_slice(&1u16.to_be_bytes()); page[5..7].copy_from_slice(&400u16.to_be_bytes()); page[7] = 0; let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2022 assert!(hdr.page_type.is_leaf());
2023 assert_eq!(hdr.header_size(), 8);
2024 }
2025
2026 #[test]
2027 fn test_btree_page_header_interior() {
2028 let page_size = PageSize::new(512).unwrap();
2029 let mut page = vec![0u8; page_size.as_usize()];
2030
2031 page[0] = 0x05;
2033 page[1..3].copy_from_slice(&0u16.to_be_bytes());
2034 page[3..5].copy_from_slice(&0u16.to_be_bytes());
2035 page[5..7].copy_from_slice(&500u16.to_be_bytes());
2036 page[7] = 0;
2037 page[8..12].copy_from_slice(&2u32.to_be_bytes()); let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2040 assert!(hdr.page_type.is_interior());
2041 assert_eq!(hdr.header_size(), 12);
2042 assert_eq!(hdr.right_most_child.unwrap().get(), 2);
2043 }
2044
2045 #[test]
2046 fn test_page1_offset_adjustment() {
2047 let page_size = PageSize::new(512).unwrap();
2048 let mut page = vec![0u8; page_size.as_usize()];
2049
2050 let h = DATABASE_HEADER_SIZE;
2052 page[h] = 0x0D; page[h + 1..h + 3].copy_from_slice(&0u16.to_be_bytes());
2054 page[h + 3..h + 5].copy_from_slice(&1u16.to_be_bytes()); page[h + 5..h + 7].copy_from_slice(&300u16.to_be_bytes()); page[h + 7] = 0;
2057
2058 page[h + 8..h + 10].copy_from_slice(&300u16.to_be_bytes());
2060
2061 let hdr = BTreePageHeader::parse(&page, page_size, 0, true).unwrap();
2062 let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
2063 assert_eq!(ptrs, vec![300u16]);
2064 }
2065
2066 #[test]
2067 fn test_cell_pointer_array() {
2068 let page_size = PageSize::new(512).unwrap();
2069 let mut page = vec![0u8; page_size.as_usize()];
2070
2071 page[0] = 0x0D;
2072 page[1..3].copy_from_slice(&0u16.to_be_bytes());
2073 page[3..5].copy_from_slice(&3u16.to_be_bytes()); page[5..7].copy_from_slice(&300u16.to_be_bytes());
2075 page[7] = 0;
2076 page[8..10].copy_from_slice(&300u16.to_be_bytes());
2077 page[10..12].copy_from_slice(&320u16.to_be_bytes());
2078 page[12..14].copy_from_slice(&340u16.to_be_bytes());
2079
2080 let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2081 let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
2082 assert_eq!(ptrs, vec![300u16, 320u16, 340u16]);
2083 }
2084
2085 #[test]
2086 fn test_freeblock_list_traversal() {
2087 let page_size = PageSize::new(512).unwrap();
2088 let mut page = vec![0u8; page_size.as_usize()];
2089
2090 page[0] = 0x0D;
2091 page[1..3].copy_from_slice(&400u16.to_be_bytes()); page[3..5].copy_from_slice(&0u16.to_be_bytes());
2093 page[5..7].copy_from_slice(&400u16.to_be_bytes());
2094 page[7] = 0;
2095
2096 page[400..402].copy_from_slice(&420u16.to_be_bytes());
2098 page[402..404].copy_from_slice(&20u16.to_be_bytes());
2099 page[420..422].copy_from_slice(&0u16.to_be_bytes());
2101 page[422..424].copy_from_slice(&30u16.to_be_bytes());
2102
2103 let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2104 let blocks = hdr.parse_freeblocks(&page, page_size, 0).unwrap();
2105 assert_eq!(
2106 blocks,
2107 vec![
2108 Freeblock {
2109 offset: 400,
2110 next: 420,
2111 size: 20
2112 },
2113 Freeblock {
2114 offset: 420,
2115 next: 0,
2116 size: 30
2117 }
2118 ]
2119 );
2120 }
2121
2122 #[test]
2123 fn test_freeblock_min_size() {
2124 let page_size = PageSize::new(512).unwrap();
2125 let mut page = vec![0u8; page_size.as_usize()];
2126
2127 page[0] = 0x0D;
2128 page[1..3].copy_from_slice(&400u16.to_be_bytes());
2129 page[3..5].copy_from_slice(&0u16.to_be_bytes());
2130 page[5..7].copy_from_slice(&400u16.to_be_bytes());
2131 page[7] = 0;
2132
2133 page[400..402].copy_from_slice(&0u16.to_be_bytes());
2134 page[402..404].copy_from_slice(&3u16.to_be_bytes()); let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2137 let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
2138 assert!(matches!(err, BTreePageError::InvalidFreeblock { .. }));
2139 }
2140
2141 #[test]
2142 fn test_fragment_defrag_threshold() {
2143 assert!(!would_exceed_fragmented_free_bytes(60, 0));
2144 assert!(would_exceed_fragmented_free_bytes(60, 1));
2145 assert!(would_exceed_fragmented_free_bytes(59, 2));
2146 }
2147
2148 #[test]
2149 fn test_e2e_bd_1a32() {
2150 use std::fs::File;
2151 use std::io::{Read, Seek};
2152 use std::process::Command;
2153 use std::sync::atomic::{AtomicUsize, Ordering};
2154
2155 static COUNTER: AtomicUsize = AtomicUsize::new(0);
2156
2157 if Command::new("sqlite3").arg("--version").output().is_err() {
2159 return;
2160 }
2161
2162 let mut path = std::env::temp_dir();
2163 path.push(format!(
2164 "fsqlite_bd_1a32_{}_{}.sqlite",
2165 std::process::id(),
2166 COUNTER.fetch_add(1, Ordering::Relaxed)
2167 ));
2168
2169 let status = Command::new("sqlite3")
2170 .arg(&path)
2171 .arg("CREATE TABLE t(x); INSERT INTO t VALUES(1);")
2172 .status()
2173 .expect("sqlite3 execution failed");
2174 assert!(status.success());
2175
2176 let mut f = File::open(&path).expect("open temp db");
2177 let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
2178 f.read_exact(&mut header_bytes).expect("read db header");
2179 let header = DatabaseHeader::from_bytes(&header_bytes).expect("parse db header");
2180 assert_eq!(header.schema_format, 4);
2181 assert_eq!(
2182 header.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
2183 DatabaseOpenMode::ReadWrite
2184 );
2185
2186 let hdr2 = header.to_bytes().expect("serialize header");
2188 assert_eq!(header_bytes, hdr2);
2189
2190 let page_size = header.page_size;
2192 let mut page1 = vec![0u8; page_size.as_usize()];
2193 f.rewind().expect("rewind");
2194 f.read_exact(&mut page1).expect("read page 1");
2195 let btree_hdr = BTreePageHeader::parse(&page1, page_size, header.reserved_per_page, true)
2196 .expect("parse page1 btree header");
2197 assert_eq!(btree_hdr.header_offset, DATABASE_HEADER_SIZE);
2198 }
2199
2200 #[test]
2201 fn test_varint_signed_cast() {
2202 use crate::serial_type::{read_varint, write_varint};
2203
2204 let test_cases: &[(u64, i64)] = &[
2206 (0, 0),
2207 (1, 1),
2208 (0x7FFF_FFFF_FFFF_FFFF, i64::MAX),
2209 (u64::MAX, -1),
2210 (0x8000_0000_0000_0000, i64::MIN),
2211 ];
2212 let mut buf = [0u8; 9];
2213 for &(unsigned, expected_signed) in test_cases {
2214 let written = write_varint(&mut buf, unsigned);
2215 let (decoded, consumed) = read_varint(&buf[..written]).unwrap();
2216 assert_eq!(decoded, unsigned);
2217 assert_eq!(consumed, written);
2218 #[allow(clippy::cast_possible_wrap)]
2219 let signed = decoded as i64;
2220 assert_eq!(
2221 signed, expected_signed,
2222 "u64 {unsigned} should cast to i64 {expected_signed}, got {signed}"
2223 );
2224 }
2225 }
2226
2227 #[test]
2228 fn test_reserved_bytes_72_91_zero() {
2229 let hdr = make_header_for_tests();
2230 let buf = hdr.to_bytes().unwrap();
2231 for (i, &byte) in buf.iter().enumerate().take(92).skip(72) {
2232 assert_eq!(byte, 0, "byte {i} should be zero (reserved region)");
2233 }
2234
2235 let mut hdr2 = make_header_for_tests();
2236 hdr2.application_id = 0xDEAD_BEEF;
2237 hdr2.user_version = 42;
2238 let buf2 = hdr2.to_bytes().unwrap();
2239 for (i, &byte) in buf2.iter().enumerate().take(92).skip(72) {
2240 assert_eq!(byte, 0, "byte {i} should be zero even with custom app_id");
2241 }
2242 }
2243
2244 #[test]
2245 fn test_version_valid_for_stale() {
2246 let mut hdr = make_header_for_tests();
2247 hdr.change_counter = 7;
2248 hdr.version_valid_for = 7;
2249 assert!(!hdr.is_page_count_stale());
2250
2251 hdr.version_valid_for = 5;
2252 assert!(hdr.is_page_count_stale());
2253
2254 hdr.page_size = PageSize::new(4096).unwrap();
2255 assert_eq!(hdr.page_count_from_file_size(4096 * 100), Some(100));
2256 assert_eq!(hdr.page_count_from_file_size(4096), Some(1));
2257 assert!(hdr.page_count_from_file_size(5000).is_none());
2258 assert!(hdr.page_count_from_file_size(0).is_none());
2259 }
2260
2261 #[test]
2262 fn test_reserved_space_per_page() {
2263 let mut hdr = make_header_for_tests();
2264 hdr.page_size = PageSize::new(4096).unwrap();
2265 hdr.reserved_per_page = 40;
2266 let usable = hdr.page_size.usable(hdr.reserved_per_page);
2267 assert_eq!(usable, 4056);
2268
2269 let buf = hdr.to_bytes().unwrap();
2270 let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
2271 assert_eq!(parsed.reserved_per_page, 40);
2272 assert_eq!(parsed.page_size.usable(parsed.reserved_per_page), 4056);
2273 }
2274
2275 #[test]
2276 fn test_header_text_encoding_invalid() {
2277 let mut buf = make_header_for_tests().to_bytes().unwrap();
2278 buf[56..60].copy_from_slice(&4u32.to_be_bytes());
2279 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2280 assert!(matches!(
2281 err,
2282 DatabaseHeaderError::InvalidTextEncoding { raw: 4 }
2283 ));
2284
2285 buf[56..60].copy_from_slice(&0u32.to_be_bytes());
2286 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2287 assert!(matches!(
2288 err,
2289 DatabaseHeaderError::InvalidTextEncoding { raw: 0 }
2290 ));
2291 }
2292
2293 #[test]
2294 fn test_btree_page_type_classification() {
2295 assert_eq!(
2296 BTreePageType::from_byte(0x02),
2297 Some(BTreePageType::InteriorIndex)
2298 );
2299 assert_eq!(
2300 BTreePageType::from_byte(0x05),
2301 Some(BTreePageType::InteriorTable)
2302 );
2303 assert_eq!(
2304 BTreePageType::from_byte(0x0A),
2305 Some(BTreePageType::LeafIndex)
2306 );
2307 assert_eq!(
2308 BTreePageType::from_byte(0x0D),
2309 Some(BTreePageType::LeafTable)
2310 );
2311
2312 assert!(BTreePageType::from_byte(0x00).is_none());
2313 assert!(BTreePageType::from_byte(0x01).is_none());
2314 assert!(BTreePageType::from_byte(0xFF).is_none());
2315
2316 assert!(BTreePageType::InteriorTable.is_interior());
2317 assert!(BTreePageType::InteriorTable.is_table());
2318 assert!(!BTreePageType::InteriorTable.is_leaf());
2319 assert!(!BTreePageType::InteriorTable.is_index());
2320
2321 assert!(BTreePageType::LeafIndex.is_leaf());
2322 assert!(BTreePageType::LeafIndex.is_index());
2323 assert!(!BTreePageType::LeafIndex.is_interior());
2324 assert!(!BTreePageType::LeafIndex.is_table());
2325 }
2326
2327 #[test]
2328 fn test_invalid_page_type_rejected() {
2329 let page_size = PageSize::new(512).unwrap();
2330 let mut page = vec![0u8; page_size.as_usize()];
2331 page[0] = 0x01;
2332 let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2333 assert!(matches!(err, BTreePageError::InvalidPageType { raw: 0x01 }));
2334 }
2335
2336 #[test]
2337 fn test_freeblock_loop_detected() {
2338 let page_size = PageSize::new(512).unwrap();
2339 let mut page = vec![0u8; page_size.as_usize()];
2340
2341 page[0] = 0x0D;
2342 page[1..3].copy_from_slice(&400u16.to_be_bytes()); page[3..5].copy_from_slice(&0u16.to_be_bytes()); page[5..7].copy_from_slice(&300u16.to_be_bytes());
2346 page[7] = 0;
2347
2348 page[400..402].copy_from_slice(&420u16.to_be_bytes());
2350 page[402..404].copy_from_slice(&20u16.to_be_bytes());
2351 page[420..422].copy_from_slice(&400u16.to_be_bytes());
2353 page[422..424].copy_from_slice(&20u16.to_be_bytes());
2354
2355 let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2356 let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
2357 assert!(matches!(err, BTreePageError::FreeblockLoop { .. }));
2358 }
2359
2360 #[test]
2361 fn test_fragmented_free_bytes_max() {
2362 let page_size = PageSize::new(512).unwrap();
2363 let mut page = vec![0u8; page_size.as_usize()];
2364
2365 page[0] = 0x0D;
2366 page[5..7].copy_from_slice(&500u16.to_be_bytes()); page[7] = 61; let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2369 assert!(matches!(
2370 err,
2371 BTreePageError::InvalidFragmentedFreeBytes { raw: 61, max: 60 }
2372 ));
2373
2374 page[7] = 60;
2376 BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2377 }
2378
2379 #[test]
2380 fn test_error_variants_distinct_display() {
2381 let errors: Vec<DatabaseHeaderError> = vec![
2382 DatabaseHeaderError::InvalidMagic,
2383 DatabaseHeaderError::InvalidPageSize { raw: 100 },
2384 DatabaseHeaderError::InvalidPayloadFractions {
2385 max: 65,
2386 min: 32,
2387 leaf: 32,
2388 },
2389 DatabaseHeaderError::UsableSizeTooSmall {
2390 page_size: 512,
2391 reserved_per_page: 33,
2392 usable_size: 479,
2393 },
2394 DatabaseHeaderError::UnsupportedReadVersion {
2395 read_version: 3,
2396 max_supported: 2,
2397 },
2398 DatabaseHeaderError::InvalidTextEncoding { raw: 4 },
2399 DatabaseHeaderError::InvalidSchemaFormat { raw: 0 },
2400 ];
2401
2402 let displays: Vec<String> = errors
2403 .iter()
2404 .map(std::string::ToString::to_string)
2405 .collect();
2406 for (i, d) in displays.iter().enumerate() {
2407 assert!(!d.is_empty(), "error variant {i} has empty display");
2408 for (j, d2) in displays.iter().enumerate() {
2409 if i != j {
2410 assert_ne!(d, d2, "error variants {i} and {j} have identical display");
2411 }
2412 }
2413 }
2414 }
2415
2416 #[test]
2419 fn test_sqlite_master_page1_root() {
2420 let page_size = PageSize::new(4096).unwrap();
2423 let mut page = [0u8; 4096];
2424 page[..16].copy_from_slice(b"SQLite format 3\0");
2427 page[16..18].copy_from_slice(&4096u16.to_be_bytes()); page[100] = 0x0D; page[103..105].copy_from_slice(&0u16.to_be_bytes());
2431 page[105..107].copy_from_slice(&4096u16.to_be_bytes()); let page_type = BTreePageType::from_byte(page[100]);
2435 assert_eq!(page_type, Some(BTreePageType::LeafTable));
2436 let hdr = BTreePageHeader::parse(&page, page_size, 0, true).expect("valid leaf header");
2437 assert_eq!(hdr.cell_count, 0, "fresh sqlite_master has 0 rows");
2438 }
2439
2440 #[test]
2441 fn test_sqlite_master_schema_columns() {
2442 let columns = ["type", "name", "tbl_name", "rootpage", "sql"];
2444 assert_eq!(columns.len(), 5);
2445 let valid_types = ["table", "index", "view", "trigger"];
2447 assert_eq!(valid_types.len(), 4);
2448 }
2449
2450 #[test]
2451 fn test_encoding_utf8_default() {
2452 let hdr = DatabaseHeader::default();
2454 assert_eq!(hdr.text_encoding, TextEncoding::Utf8);
2455
2456 let bytes = hdr.to_bytes().expect("encode");
2457 let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2459 assert_eq!(enc_raw, 1, "UTF-8 encoding stored as 1 at offset 56");
2460 }
2461
2462 #[test]
2463 fn test_encoding_utf16le() {
2464 let mut hdr = make_header_for_tests();
2465 hdr.text_encoding = TextEncoding::Utf16le;
2466 let bytes = hdr.to_bytes().expect("encode");
2467 let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2468 assert_eq!(enc_raw, 2, "UTF-16LE encoding stored as 2");
2469
2470 let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2471 assert_eq!(parsed.text_encoding, TextEncoding::Utf16le);
2472 }
2473
2474 #[test]
2475 fn test_encoding_utf16be() {
2476 let mut hdr = make_header_for_tests();
2477 hdr.text_encoding = TextEncoding::Utf16be;
2478 let bytes = hdr.to_bytes().expect("encode");
2479 let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2480 assert_eq!(enc_raw, 3, "UTF-16BE encoding stored as 3");
2481
2482 let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2483 assert_eq!(parsed.text_encoding, TextEncoding::Utf16be);
2484 }
2485
2486 #[test]
2487 fn test_encoding_immutable_after_creation() {
2488 let hdr1 = make_header_for_tests();
2493 assert_eq!(hdr1.text_encoding, TextEncoding::Utf8);
2494 let bytes1 = hdr1.to_bytes().expect("encode");
2495
2496 let mut hdr2 = hdr1;
2497 hdr2.text_encoding = TextEncoding::Utf16le;
2498 let bytes2 = hdr2.to_bytes().expect("encode");
2499
2500 assert_ne!(
2502 bytes1[56..60],
2503 bytes2[56..60],
2504 "different encodings must serialize differently"
2505 );
2506 }
2507
2508 #[test]
2509 fn test_binary_collation_memcmp_utf8() {
2510 let a = "abc";
2513 let b = "abd";
2514 assert!(
2515 a.as_bytes() < b.as_bytes(),
2516 "memcmp ordering for ASCII UTF-8"
2517 );
2518
2519 let z = "z";
2523 let e_acute = "é";
2524 assert!(
2525 z.as_bytes() < e_acute.as_bytes(),
2526 "UTF-8 memcmp preserves code point order"
2527 );
2528 }
2529
2530 #[test]
2533 fn test_affinity_int_keyword() {
2534 assert_eq!(
2535 TypeAffinity::from_type_name("INTEGER"),
2536 TypeAffinity::Integer
2537 );
2538 assert_eq!(TypeAffinity::from_type_name("INT"), TypeAffinity::Integer);
2539 assert_eq!(
2540 TypeAffinity::from_type_name("TINYINT"),
2541 TypeAffinity::Integer
2542 );
2543 assert_eq!(
2544 TypeAffinity::from_type_name("SMALLINT"),
2545 TypeAffinity::Integer
2546 );
2547 assert_eq!(
2548 TypeAffinity::from_type_name("MEDIUMINT"),
2549 TypeAffinity::Integer
2550 );
2551 assert_eq!(
2552 TypeAffinity::from_type_name("BIGINT"),
2553 TypeAffinity::Integer
2554 );
2555 assert_eq!(
2556 TypeAffinity::from_type_name("UNSIGNED BIG INT"),
2557 TypeAffinity::Integer
2558 );
2559 assert_eq!(TypeAffinity::from_type_name("INT2"), TypeAffinity::Integer);
2560 assert_eq!(TypeAffinity::from_type_name("INT8"), TypeAffinity::Integer);
2561 }
2562
2563 #[test]
2564 fn test_affinity_text_keyword() {
2565 assert_eq!(TypeAffinity::from_type_name("TEXT"), TypeAffinity::Text);
2566 assert_eq!(
2567 TypeAffinity::from_type_name("CHARACTER(20)"),
2568 TypeAffinity::Text
2569 );
2570 assert_eq!(
2571 TypeAffinity::from_type_name("VARCHAR(255)"),
2572 TypeAffinity::Text
2573 );
2574 assert_eq!(
2575 TypeAffinity::from_type_name("VARYING CHARACTER(255)"),
2576 TypeAffinity::Text
2577 );
2578 assert_eq!(
2579 TypeAffinity::from_type_name("NCHAR(55)"),
2580 TypeAffinity::Text
2581 );
2582 assert_eq!(
2583 TypeAffinity::from_type_name("NATIVE CHARACTER(70)"),
2584 TypeAffinity::Text
2585 );
2586 assert_eq!(
2587 TypeAffinity::from_type_name("NVARCHAR(100)"),
2588 TypeAffinity::Text
2589 );
2590 assert_eq!(TypeAffinity::from_type_name("CLOB"), TypeAffinity::Text);
2591 }
2592
2593 #[test]
2594 fn test_affinity_blob_keyword() {
2595 assert_eq!(TypeAffinity::from_type_name("BLOB"), TypeAffinity::Blob);
2596 assert_eq!(TypeAffinity::from_type_name("blob"), TypeAffinity::Blob);
2597 }
2598
2599 #[test]
2600 fn test_affinity_empty_type() {
2601 assert_eq!(TypeAffinity::from_type_name(""), TypeAffinity::Blob);
2602 }
2603
2604 #[test]
2605 fn test_affinity_real_keyword() {
2606 assert_eq!(TypeAffinity::from_type_name("REAL"), TypeAffinity::Real);
2607 assert_eq!(TypeAffinity::from_type_name("DOUBLE"), TypeAffinity::Real);
2608 assert_eq!(
2609 TypeAffinity::from_type_name("DOUBLE PRECISION"),
2610 TypeAffinity::Real
2611 );
2612 assert_eq!(TypeAffinity::from_type_name("FLOAT"), TypeAffinity::Real);
2613 }
2614
2615 #[test]
2616 fn test_affinity_numeric_keyword() {
2617 assert_eq!(
2618 TypeAffinity::from_type_name("NUMERIC"),
2619 TypeAffinity::Numeric
2620 );
2621 assert_eq!(
2622 TypeAffinity::from_type_name("DECIMAL(10,5)"),
2623 TypeAffinity::Numeric
2624 );
2625 assert_eq!(
2626 TypeAffinity::from_type_name("BOOLEAN"),
2627 TypeAffinity::Numeric
2628 );
2629 assert_eq!(TypeAffinity::from_type_name("DATE"), TypeAffinity::Numeric);
2630 assert_eq!(
2631 TypeAffinity::from_type_name("DATETIME"),
2632 TypeAffinity::Numeric
2633 );
2634 }
2635
2636 #[test]
2637 fn test_affinity_case_insensitive() {
2638 assert_eq!(
2639 TypeAffinity::from_type_name("integer"),
2640 TypeAffinity::Integer
2641 );
2642 assert_eq!(TypeAffinity::from_type_name("text"), TypeAffinity::Text);
2643 assert_eq!(TypeAffinity::from_type_name("Real"), TypeAffinity::Real);
2644 assert_eq!(
2645 TypeAffinity::from_type_name("Numeric"),
2646 TypeAffinity::Numeric
2647 );
2648 }
2649
2650 #[test]
2651 fn test_affinity_first_match_int_before_char() {
2652 assert_eq!(
2654 TypeAffinity::from_type_name("CHARINT"),
2655 TypeAffinity::Integer
2656 );
2657 assert_eq!(
2659 TypeAffinity::from_type_name("POINTERFLOAT"),
2660 TypeAffinity::Integer
2661 );
2662 }
2663
2664 #[test]
2665 fn test_comparison_numeric_vs_text() {
2666 assert_eq!(
2667 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Text),
2668 Some(TypeAffinity::Numeric)
2669 );
2670 assert_eq!(
2671 TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Real),
2672 Some(TypeAffinity::Numeric)
2673 );
2674 assert_eq!(
2675 TypeAffinity::comparison_affinity(TypeAffinity::Numeric, TypeAffinity::Blob),
2676 Some(TypeAffinity::Numeric)
2677 );
2678 }
2679
2680 #[test]
2681 fn test_comparison_text_vs_blob() {
2682 assert_eq!(
2683 TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Blob),
2684 Some(TypeAffinity::Text)
2685 );
2686 assert_eq!(
2687 TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Text),
2688 Some(TypeAffinity::Text)
2689 );
2690 }
2691
2692 #[test]
2693 fn test_comparison_same_affinity_no_coercion() {
2694 assert_eq!(
2695 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Integer),
2696 None
2697 );
2698 assert_eq!(
2699 TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Text),
2700 None
2701 );
2702 assert_eq!(
2703 TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2704 None
2705 );
2706 }
2707
2708 #[test]
2709 fn test_comparison_both_blob_no_coercion() {
2710 assert_eq!(
2711 TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2712 None
2713 );
2714 }
2715
2716 #[test]
2717 fn test_affinity_applied_to_needing_operand_only() {
2718 let left = SqliteValue::Integer(42);
2719 let right = SqliteValue::Text(SmallText::new("123"));
2720 let affinity = TypeAffinity::comparison_affinity(left.affinity(), right.affinity())
2721 .expect("numeric-vs-text comparison must request numeric coercion");
2722
2723 let left_after = left.clone();
2725 let right_after = right.apply_affinity(affinity);
2727
2728 assert_eq!(left_after, left);
2729 assert_eq!(right_after, SqliteValue::Integer(123));
2730 }
2731
2732 #[test]
2733 fn test_comparison_numeric_subtypes() {
2734 assert_eq!(
2737 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Real),
2738 None
2739 );
2740 assert_eq!(
2741 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Numeric),
2742 None
2743 );
2744 assert_eq!(
2745 TypeAffinity::comparison_affinity(TypeAffinity::Real, TypeAffinity::Numeric),
2746 None
2747 );
2748 }
2749
2750 #[test]
2753 fn test_write_empty_leaf_table_basic() {
2754 let ps = PageSize::DEFAULT;
2755 let mut buf = vec![0u8; ps.as_usize()];
2756 BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2757
2758 assert_eq!(buf[0], 0x0D, "page type LeafTable");
2759 assert_eq!(buf[1], 0, "first_freeblock hi");
2760 assert_eq!(buf[2], 0, "first_freeblock lo");
2761 assert_eq!(buf[3], 0, "cell_count hi");
2762 assert_eq!(buf[4], 0, "cell_count lo");
2763 assert_eq!(buf[5], 0x10, "content_offset hi");
2765 assert_eq!(buf[6], 0x00, "content_offset lo");
2766 assert_eq!(buf[7], 0, "fragmented_free_bytes");
2767 }
2768
2769 #[test]
2770 fn test_write_empty_leaf_table_page1_offset() {
2771 let ps = PageSize::DEFAULT;
2772 let mut buf = vec![0u8; ps.as_usize()];
2773 BTreePageHeader::write_empty_leaf_table(&mut buf, DATABASE_HEADER_SIZE, ps.get());
2774
2775 assert_eq!(buf[DATABASE_HEADER_SIZE], 0x0D, "page type at offset 100");
2776 assert!(buf[..DATABASE_HEADER_SIZE].iter().all(|&b| b == 0));
2778 }
2779
2780 #[test]
2781 fn test_write_empty_leaf_table_65536_encoding() {
2782 let ps = PageSize::new(65536).unwrap();
2783 let mut buf = vec![0u8; ps.as_usize()];
2784 BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2785
2786 assert_eq!(buf[5], 0x00, "65536 encoded as 0 hi");
2788 assert_eq!(buf[6], 0x00, "65536 encoded as 0 lo");
2789 }
2790
2791 #[test]
2792 fn test_write_empty_leaf_table_512_page_size() {
2793 let ps = PageSize::new(512).unwrap();
2794 let mut buf = vec![0u8; ps.as_usize()];
2795 BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2796
2797 assert_eq!(buf[5], 0x02, "512 hi byte");
2799 assert_eq!(buf[6], 0x00, "512 lo byte");
2800 }
2801}