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