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