1pub mod cx;
2pub mod ecs;
3pub mod encoding;
4pub mod flags;
5pub mod glossary;
6pub mod limits;
7pub mod obligation;
8pub mod opcode;
9pub mod record;
10pub mod serial_type;
11pub mod value;
12
13pub use cx::Cx;
14pub use ecs::{
15 ObjectId, PayloadHash, SYMBOL_RECORD_MAGIC, SYMBOL_RECORD_VERSION, SymbolReadPath,
16 SymbolRecord, SymbolRecordError, SymbolRecordFlags, SystematicLayoutError,
17 layout_systematic_run, reconstruct_systematic_happy_path, recover_object_with_fallback,
18 source_symbol_count, validate_systematic_run,
19};
20pub use glossary::{
21 ArcCache, BtreeRef, Budget, COMMIT_MARKER_RECORD_V1_SIZE, ColumnIdx, CommitCapsule,
22 CommitMarker, CommitProof, CommitSeq, DecodeProof, DependencyEdge, EpochId, IdempotencyKey,
23 IndexId, IntentFootprint, IntentLog, IntentOp, IntentOpKind, OTI_WIRE_SIZE, OperatingMode, Oti,
24 Outcome, PageHistory, PageVersion, RangeKey, ReadWitness, RebaseBinaryOp, RebaseExpr,
25 RebaseUnaryOp, Region, RemoteCap, RootManifest, RowId, RowIdAllocator, RowIdExhausted,
26 RowIdMode, Saga, SchemaEpoch, SemanticKeyKind, SemanticKeyRef, Snapshot, StructuralEffects,
27 SymbolAuthMasterKeyCap, SymbolValidityWindow, TableId, TxnEpoch, TxnId, TxnSlot, TxnToken,
28 VersionPointer, WitnessIndexSegment, WitnessKey, WriteWitness,
29};
30pub use value::SqliteValue;
31
32use std::fmt;
33use std::num::NonZeroU32;
34
35#[derive(
40 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
41)]
42#[repr(transparent)]
43pub struct PageNumber(NonZeroU32);
44
45impl PageNumber {
46 pub const ONE: Self = Self(NonZeroU32::MIN);
49
50 #[inline]
54 pub const fn new(n: u32) -> Option<Self> {
55 match NonZeroU32::new(n) {
56 Some(v) => Some(Self(v)),
57 None => None,
58 }
59 }
60
61 #[inline]
63 pub const fn get(self) -> u32 {
64 self.0.get()
65 }
66}
67
68impl fmt::Display for PageNumber {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{}", self.0)
71 }
72}
73
74impl TryFrom<u32> for PageNumber {
75 type Error = InvalidPageNumber;
76
77 fn try_from(value: u32) -> Result<Self, Self::Error> {
78 Self::new(value).ok_or(InvalidPageNumber)
79 }
80}
81
82#[derive(Default)]
87pub struct PageNumberHasher(u64);
88
89impl std::hash::Hasher for PageNumberHasher {
90 fn write(&mut self, _: &[u8]) {
91 debug_assert!(false, "PageNumberHasher only supports write_u32");
94 }
95
96 fn write_u32(&mut self, n: u32) {
97 self.0 = u64::from(n);
98 }
99
100 fn finish(&self) -> u64 {
101 self.0
102 }
103}
104
105pub type PageNumberBuildHasher = std::hash::BuildHasherDefault<PageNumberHasher>;
107
108#[must_use]
110pub const fn gf256_add_byte(lhs: u8, rhs: u8) -> u8 {
111 lhs ^ rhs
112}
113
114#[must_use]
119pub fn gf256_mul_byte(mut a: u8, mut b: u8) -> u8 {
120 let mut out = 0_u8;
121 while b != 0 {
122 if (b & 1) != 0 {
123 out ^= a;
124 }
125 let carry = (a & 0x80) != 0;
126 a <<= 1;
127 if carry {
128 a ^= 0x1D;
129 }
130 b >>= 1;
131 }
132 out
133}
134
135#[must_use]
137pub fn gf256_inverse_byte(value: u8) -> Option<u8> {
138 if value == 0 {
139 return None;
140 }
141 for candidate in 1u16..=255 {
142 let inv = u8::try_from(candidate).expect("candidate in 1..=255 always fits u8");
143 if gf256_mul_byte(value, inv) == 1 {
144 return Some(inv);
145 }
146 }
147 None
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
152pub enum MergePageKind {
153 BtreeInteriorTable,
155 BtreeLeafTable,
157 BtreeInteriorIndex,
159 BtreeLeafIndex,
161 Overflow,
163 Freelist,
165 PointerMap,
167 Opaque,
169}
170
171impl MergePageKind {
172 #[must_use]
174 pub const fn is_sqlite_structured(self) -> bool {
175 !matches!(self, Self::Opaque)
176 }
177
178 #[must_use]
180 pub fn classify(page: &[u8]) -> Self {
181 let Some(first_byte) = page.first().copied() else {
182 return Self::Opaque;
183 };
184 match BTreePageType::from_byte(first_byte) {
185 Some(BTreePageType::LeafTable) => Self::BtreeLeafTable,
186 Some(BTreePageType::InteriorTable) => Self::BtreeInteriorTable,
187 Some(BTreePageType::LeafIndex) => Self::BtreeLeafIndex,
188 Some(BTreePageType::InteriorIndex) => Self::BtreeInteriorIndex,
189 None => Self::Opaque,
190 }
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub struct InvalidPageNumber;
197
198impl fmt::Display for InvalidPageNumber {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 f.write_str("page number cannot be zero")
201 }
202}
203
204impl std::error::Error for InvalidPageNumber {}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211pub struct PageSize(u32);
212
213impl PageSize {
214 pub const MIN: Self = Self(512);
216
217 pub const DEFAULT: Self = Self(limits::DEFAULT_PAGE_SIZE);
219
220 pub const MAX: Self = Self(limits::MAX_PAGE_SIZE);
222
223 pub const fn new(size: u32) -> Option<Self> {
226 if size < 512 || size > 65536 || !size.is_power_of_two() {
227 None
228 } else {
229 Some(Self(size))
230 }
231 }
232
233 #[inline]
235 pub const fn get(self) -> u32 {
236 self.0
237 }
238
239 #[inline]
241 pub const fn as_usize(self) -> usize {
242 self.0 as usize
243 }
244
245 #[inline]
250 pub const fn usable(self, reserved: u8) -> u32 {
251 self.0 - reserved as u32
252 }
253}
254
255impl Default for PageSize {
256 fn default() -> Self {
257 Self::DEFAULT
258 }
259}
260
261impl fmt::Display for PageSize {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 write!(f, "{}", self.0)
264 }
265}
266
267#[derive(Clone, PartialEq, Eq)]
272pub struct PageData {
273 data: std::sync::Arc<Vec<u8>>,
274}
275
276impl PageData {
277 pub fn zeroed(size: PageSize) -> Self {
279 Self {
280 data: std::sync::Arc::new(vec![0u8; size.as_usize()]),
281 }
282 }
283
284 pub fn from_vec(data: Vec<u8>) -> Self {
287 Self {
288 data: std::sync::Arc::new(data),
289 }
290 }
291
292 #[inline]
294 pub fn as_bytes(&self) -> &[u8] {
295 &self.data
296 }
297
298 #[inline]
302 pub fn as_bytes_mut(&mut self) -> &mut [u8] {
303 std::sync::Arc::make_mut(&mut self.data).as_mut_slice()
304 }
305
306 #[inline]
308 pub fn len(&self) -> usize {
309 self.data.len()
310 }
311
312 #[inline]
314 pub fn is_empty(&self) -> bool {
315 self.data.is_empty()
316 }
317
318 pub fn into_vec(self) -> Vec<u8> {
322 match std::sync::Arc::try_unwrap(self.data) {
323 Ok(v) => v,
324 Err(arc) => (*arc).clone(),
325 }
326 }
327}
328
329impl fmt::Debug for PageData {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 f.debug_struct("PageData")
332 .field("len", &self.data.len())
333 .finish()
334 }
335}
336
337impl AsRef<[u8]> for PageData {
338 fn as_ref(&self) -> &[u8] {
339 &self.data
340 }
341}
342
343impl AsMut<[u8]> for PageData {
344 fn as_mut(&mut self) -> &mut [u8] {
345 std::sync::Arc::make_mut(&mut self.data).as_mut_slice()
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
353#[repr(u8)]
354pub enum TypeAffinity {
355 Integer = b'D',
357 Text = b'B',
359 Blob = b'A',
361 Real = b'E',
363 Numeric = b'C',
366}
367
368impl TypeAffinity {
369 pub fn from_type_name(type_name: &str) -> Self {
378 let upper = type_name.to_ascii_uppercase();
379
380 if upper.contains("INT") {
381 Self::Integer
382 } else if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
383 Self::Text
384 } else if upper.is_empty() || upper.contains("BLOB") {
385 Self::Blob
386 } else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
387 Self::Real
388 } else {
389 Self::Numeric
390 }
391 }
392
393 pub fn comparison_affinity(left: Self, right: Self) -> Option<Self> {
406 if left == right {
407 return None;
408 }
409
410 let is_numeric = |a: Self| matches!(a, Self::Integer | Self::Real | Self::Numeric);
411
412 if is_numeric(left) && matches!(right, Self::Text | Self::Blob) {
414 return Some(Self::Numeric);
415 }
416 if is_numeric(right) && matches!(left, Self::Text | Self::Blob) {
417 return Some(Self::Numeric);
418 }
419
420 if (left == Self::Text && right == Self::Blob)
422 || (left == Self::Blob && right == Self::Text)
423 {
424 return Some(Self::Text);
425 }
426
427 None
429 }
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
437#[repr(u8)]
438pub enum StorageClass {
439 Null = 1,
441 Integer = 2,
443 Real = 3,
445 Text = 4,
447 Blob = 5,
449}
450
451impl fmt::Display for StorageClass {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 match self {
454 Self::Null => f.write_str("NULL"),
455 Self::Integer => f.write_str("INTEGER"),
456 Self::Real => f.write_str("REAL"),
457 Self::Text => f.write_str("TEXT"),
458 Self::Blob => f.write_str("BLOB"),
459 }
460 }
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
468pub enum StrictColumnType {
469 Integer,
471 Real,
473 Text,
475 Blob,
477 Any,
479}
480
481impl StrictColumnType {
482 pub fn from_type_name(name: &str) -> Option<Self> {
487 match name.to_ascii_uppercase().as_str() {
488 "INT" | "INTEGER" => Some(Self::Integer),
489 "REAL" => Some(Self::Real),
490 "TEXT" => Some(Self::Text),
491 "BLOB" => Some(Self::Blob),
492 "ANY" => Some(Self::Any),
493 _ => None,
494 }
495 }
496}
497
498#[derive(Debug, Clone, PartialEq, Eq)]
500pub struct StrictTypeError {
501 pub expected: StrictColumnType,
503 pub actual: StorageClass,
505}
506
507impl fmt::Display for StrictTypeError {
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509 write!(
510 f,
511 "cannot store {} value in {:?} column",
512 self.actual, self.expected
513 )
514 }
515}
516
517#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
519#[repr(u8)]
520pub enum TextEncoding {
521 #[default]
523 Utf8 = 1,
524 Utf16le = 2,
526 Utf16be = 3,
528}
529
530#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
532pub enum JournalMode {
533 #[default]
535 Delete,
536 Truncate,
538 Persist,
540 Memory,
542 Wal,
544 Off,
546}
547
548#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
550#[repr(u8)]
551pub enum SynchronousMode {
552 Off = 0,
554 Normal = 1,
556 #[default]
558 Full = 2,
559 Extra = 3,
561}
562
563#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
565#[repr(u8)]
566pub enum LockLevel {
567 #[default]
569 None = 0,
570 Shared = 1,
572 Reserved = 2,
574 Pending = 3,
576 Exclusive = 4,
578}
579
580#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
582#[repr(u8)]
583pub enum CheckpointMode {
584 Passive = 0,
586 Full = 1,
588 Restart = 2,
590 Truncate = 3,
592}
593
594#[derive(Debug, Clone, PartialEq, Eq)]
599pub struct DatabaseHeader {
600 pub page_size: PageSize,
602 pub write_version: u8,
604 pub read_version: u8,
606 pub reserved_per_page: u8,
608 pub change_counter: u32,
610 pub page_count: u32,
612 pub freelist_trunk: u32,
614 pub freelist_count: u32,
616 pub schema_cookie: u32,
618 pub schema_format: u32,
620 pub default_cache_size: i32,
622 pub largest_root_page: u32,
624 pub text_encoding: TextEncoding,
626 pub user_version: u32,
628 pub incremental_vacuum: u32,
630 pub application_id: u32,
632 pub version_valid_for: u32,
635 pub sqlite_version: u32,
637}
638
639impl Default for DatabaseHeader {
640 fn default() -> Self {
641 Self {
642 page_size: PageSize::DEFAULT,
643 write_version: 1,
644 read_version: 1,
645 reserved_per_page: 0,
646 change_counter: 0,
647 page_count: 0,
648 freelist_trunk: 0,
649 freelist_count: 0,
650 schema_cookie: 0,
651 schema_format: 4,
652 default_cache_size: -2000,
653 largest_root_page: 0,
654 text_encoding: TextEncoding::Utf8,
655 user_version: 0,
656 incremental_vacuum: 0,
657 application_id: 0,
658 version_valid_for: 0,
659 sqlite_version: 0,
660 }
661 }
662}
663
664pub const DATABASE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
666
667pub const DATABASE_HEADER_SIZE: usize = 100;
669
670pub const MAX_FILE_FORMAT_VERSION: u8 = 2;
676
677pub const FRANKENSQLITE_SQLITE_VERSION_NUMBER: u32 = 3_052_000;
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
684pub enum DatabaseOpenMode {
685 ReadWrite,
687 ReadOnly,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq)]
693pub enum DatabaseHeaderError {
694 InvalidMagic,
696 InvalidPageSize { raw: u16 },
698 InvalidPayloadFractions { max: u8, min: u8, leaf: u8 },
700 UsableSizeTooSmall {
702 page_size: u32,
703 reserved_per_page: u8,
704 usable_size: u32,
705 },
706 UnsupportedReadVersion { read_version: u8, max_supported: u8 },
708 InvalidTextEncoding { raw: u32 },
710 InvalidSchemaFormat { raw: u32 },
712}
713
714impl fmt::Display for DatabaseHeaderError {
715 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
716 match self {
717 Self::InvalidMagic => f.write_str("invalid database header magic"),
718 Self::InvalidPageSize { raw } => write!(f, "invalid page size encoding: {raw}"),
719 Self::InvalidPayloadFractions { max, min, leaf } => write!(
720 f,
721 "invalid payload fractions: max={max} min={min} leaf={leaf}"
722 ),
723 Self::UsableSizeTooSmall {
724 page_size,
725 reserved_per_page,
726 usable_size,
727 } => write!(
728 f,
729 "usable page size too small: page_size={page_size} reserved={reserved_per_page} usable={usable_size}"
730 ),
731 Self::UnsupportedReadVersion {
732 read_version,
733 max_supported,
734 } => write!(
735 f,
736 "unsupported read format version: read_version={read_version} max_supported={max_supported}"
737 ),
738 Self::InvalidTextEncoding { raw } => write!(f, "invalid text encoding: {raw}"),
739 Self::InvalidSchemaFormat { raw } => write!(f, "invalid schema format: {raw}"),
740 }
741 }
742}
743
744impl std::error::Error for DatabaseHeaderError {}
745
746impl DatabaseHeader {
747 pub fn from_bytes(buf: &[u8; DATABASE_HEADER_SIZE]) -> Result<Self, DatabaseHeaderError> {
749 if &buf[..DATABASE_HEADER_MAGIC.len()] != DATABASE_HEADER_MAGIC {
750 return Err(DatabaseHeaderError::InvalidMagic);
751 }
752
753 let page_size_raw = encoding::read_u16_be(&buf[16..18]).expect("fixed u16 field");
754 let page_size_u32 = match page_size_raw {
755 1 => 65_536,
756 0 => return Err(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw }),
757 n => u32::from(n),
758 };
759 let page_size = PageSize::new(page_size_u32)
760 .ok_or(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw })?;
761
762 let write_version = buf[18];
763 let read_version = buf[19];
764 let reserved_per_page = buf[20];
765
766 let max_payload = buf[21];
767 let min_payload = buf[22];
768 let leaf_payload = buf[23];
769 if (max_payload, min_payload, leaf_payload) != (64, 32, 32) {
770 return Err(DatabaseHeaderError::InvalidPayloadFractions {
771 max: max_payload,
772 min: min_payload,
773 leaf: leaf_payload,
774 });
775 }
776
777 let usable_size = page_size.usable(reserved_per_page);
778 if usable_size < 480 {
779 return Err(DatabaseHeaderError::UsableSizeTooSmall {
780 page_size: page_size.get(),
781 reserved_per_page,
782 usable_size,
783 });
784 }
785
786 if read_version > MAX_FILE_FORMAT_VERSION {
788 return Err(DatabaseHeaderError::UnsupportedReadVersion {
789 read_version,
790 max_supported: MAX_FILE_FORMAT_VERSION,
791 });
792 }
793
794 let change_counter = encoding::read_u32_be(&buf[24..28]).expect("fixed u32 field");
795 let page_count = encoding::read_u32_be(&buf[28..32]).expect("fixed u32 field");
796 let freelist_trunk = encoding::read_u32_be(&buf[32..36]).expect("fixed u32 field");
797 let freelist_count = encoding::read_u32_be(&buf[36..40]).expect("fixed u32 field");
798 let schema_cookie = encoding::read_u32_be(&buf[40..44]).expect("fixed u32 field");
799 let schema_format = encoding::read_u32_be(&buf[44..48]).expect("fixed u32 field");
800
801 if schema_format != 4 {
804 return Err(DatabaseHeaderError::InvalidSchemaFormat { raw: schema_format });
805 }
806
807 let default_cache_size = encoding::read_i32_be(&buf[48..52]).expect("fixed i32 field");
808 let largest_root_page = encoding::read_u32_be(&buf[52..56]).expect("fixed u32 field");
809
810 let text_encoding_raw = encoding::read_u32_be(&buf[56..60]).expect("fixed u32 field");
811 let text_encoding = match text_encoding_raw {
812 1 => TextEncoding::Utf8,
813 2 => TextEncoding::Utf16le,
814 3 => TextEncoding::Utf16be,
815 _ => {
816 return Err(DatabaseHeaderError::InvalidTextEncoding {
817 raw: text_encoding_raw,
818 });
819 }
820 };
821
822 let user_version = encoding::read_u32_be(&buf[60..64]).expect("fixed u32 field");
823 let incremental_vacuum = encoding::read_u32_be(&buf[64..68]).expect("fixed u32 field");
824 let application_id = encoding::read_u32_be(&buf[68..72]).expect("fixed u32 field");
825 let version_valid_for = encoding::read_u32_be(&buf[92..96]).expect("fixed u32 field");
826 let sqlite_version = encoding::read_u32_be(&buf[96..100]).expect("fixed u32 field");
827
828 Ok(Self {
829 page_size,
830 write_version,
831 read_version,
832 reserved_per_page,
833 change_counter,
834 page_count,
835 freelist_trunk,
836 freelist_count,
837 schema_cookie,
838 schema_format,
839 default_cache_size,
840 largest_root_page,
841 text_encoding,
842 user_version,
843 incremental_vacuum,
844 application_id,
845 version_valid_for,
846 sqlite_version,
847 })
848 }
849
850 pub const fn open_mode(
852 &self,
853 max_supported: u8,
854 ) -> Result<DatabaseOpenMode, DatabaseHeaderError> {
855 if self.read_version > max_supported {
856 return Err(DatabaseHeaderError::UnsupportedReadVersion {
857 read_version: self.read_version,
858 max_supported,
859 });
860 }
861 if self.write_version > max_supported {
862 return Ok(DatabaseOpenMode::ReadOnly);
863 }
864 Ok(DatabaseOpenMode::ReadWrite)
865 }
866
867 pub const fn is_page_count_stale(&self) -> bool {
874 self.version_valid_for != self.change_counter
875 }
876
877 #[allow(clippy::cast_possible_truncation)]
883 pub const fn page_count_from_file_size(&self, file_size: u64) -> Option<u32> {
884 let ps = self.page_size.get() as u64;
885 if file_size == 0 || file_size % ps != 0 {
886 return None;
887 }
888 let count = file_size / ps;
889 if count > u32::MAX as u64 {
890 return None;
891 }
892 Some(count as u32)
893 }
894
895 pub fn write_to_bytes(
897 &self,
898 out: &mut [u8; DATABASE_HEADER_SIZE],
899 ) -> Result<(), DatabaseHeaderError> {
900 if self.schema_format != 4 {
902 return Err(DatabaseHeaderError::InvalidSchemaFormat {
903 raw: self.schema_format,
904 });
905 }
906
907 let usable_size = self.page_size.usable(self.reserved_per_page);
908 if usable_size < 480 {
909 return Err(DatabaseHeaderError::UsableSizeTooSmall {
910 page_size: self.page_size.get(),
911 reserved_per_page: self.reserved_per_page,
912 usable_size,
913 });
914 }
915
916 out.fill(0);
917 out[..DATABASE_HEADER_MAGIC.len()].copy_from_slice(DATABASE_HEADER_MAGIC);
918
919 let page_size_raw = if self.page_size.get() == 65_536 {
921 1u16
922 } else {
923 #[allow(clippy::cast_possible_truncation)]
924 {
925 self.page_size.get() as u16
926 }
927 };
928 encoding::write_u16_be(&mut out[16..18], page_size_raw).expect("fixed u16 field");
929
930 out[18] = self.write_version;
931 out[19] = self.read_version;
932 out[20] = self.reserved_per_page;
933
934 out[21] = 64;
936 out[22] = 32;
937 out[23] = 32;
938
939 encoding::write_u32_be(&mut out[24..28], self.change_counter).expect("fixed u32 field");
940 encoding::write_u32_be(&mut out[28..32], self.page_count).expect("fixed u32 field");
941 encoding::write_u32_be(&mut out[32..36], self.freelist_trunk).expect("fixed u32 field");
942 encoding::write_u32_be(&mut out[36..40], self.freelist_count).expect("fixed u32 field");
943 encoding::write_u32_be(&mut out[40..44], self.schema_cookie).expect("fixed u32 field");
944 encoding::write_u32_be(&mut out[44..48], self.schema_format).expect("fixed u32 field");
945 encoding::write_i32_be(&mut out[48..52], self.default_cache_size).expect("fixed i32 field");
946 encoding::write_u32_be(&mut out[52..56], self.largest_root_page).expect("fixed u32 field");
947
948 let text_encoding_u32 = match self.text_encoding {
949 TextEncoding::Utf8 => 1u32,
950 TextEncoding::Utf16le => 2u32,
951 TextEncoding::Utf16be => 3u32,
952 };
953 encoding::write_u32_be(&mut out[56..60], text_encoding_u32).expect("fixed u32 field");
954
955 encoding::write_u32_be(&mut out[60..64], self.user_version).expect("fixed u32 field");
956 encoding::write_u32_be(&mut out[64..68], self.incremental_vacuum).expect("fixed u32 field");
957 encoding::write_u32_be(&mut out[68..72], self.application_id).expect("fixed u32 field");
958
959 encoding::write_u32_be(&mut out[92..96], self.version_valid_for).expect("fixed u32 field");
961 encoding::write_u32_be(&mut out[96..100], self.sqlite_version).expect("fixed u32 field");
962
963 Ok(())
964 }
965
966 pub fn to_bytes(&self) -> Result<[u8; DATABASE_HEADER_SIZE], DatabaseHeaderError> {
968 let mut out = [0u8; DATABASE_HEADER_SIZE];
969 self.write_to_bytes(&mut out)?;
970 Ok(out)
971 }
972}
973
974pub const BTREE_MAX_FRAGMENTED_FREE_BYTES: u8 = 60;
976
977#[derive(Debug, Clone, PartialEq, Eq)]
979pub enum BTreePageError {
980 PageSizeMismatch { expected: usize, actual: usize },
982 PageTooSmall { usable_size: usize, needed: usize },
984 InvalidPageType { raw: u8 },
986 InvalidFragmentedFreeBytes { raw: u8, max: u8 },
988 InvalidCellContentAreaStart {
990 raw: u16,
991 decoded: u32,
992 usable_size: usize,
993 },
994 CellContentAreaOverlapsCellPointers {
996 cell_content_start: u32,
997 cell_pointer_array_end: usize,
998 },
999 CellPointerArrayOutOfBounds {
1001 start: usize,
1002 len: usize,
1003 usable_size: usize,
1004 },
1005 InvalidCellPointer {
1007 index: usize,
1008 offset: u16,
1009 usable_size: usize,
1010 },
1011 InvalidFreeblock {
1013 offset: u16,
1014 size: u16,
1015 usable_size: usize,
1016 },
1017 FreeblockLoop { offset: u16 },
1019 InvalidRightMostChild { raw: u32 },
1021}
1022
1023impl fmt::Display for BTreePageError {
1024 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1025 match self {
1026 Self::PageSizeMismatch { expected, actual } => write!(
1027 f,
1028 "page size mismatch: expected {expected} bytes, got {actual} bytes"
1029 ),
1030 Self::PageTooSmall {
1031 usable_size,
1032 needed,
1033 } => write!(
1034 f,
1035 "page too small: usable_size={usable_size} needed={needed}"
1036 ),
1037 Self::InvalidPageType { raw } => write!(f, "invalid B-tree page type: {raw:#04x}"),
1038 Self::InvalidFragmentedFreeBytes { raw, max } => {
1039 write!(f, "invalid fragmented free bytes: {raw} (max {max})")
1040 }
1041 Self::InvalidCellContentAreaStart {
1042 raw,
1043 decoded,
1044 usable_size,
1045 } => write!(
1046 f,
1047 "invalid cell content area start: raw={raw} decoded={decoded} usable_size={usable_size}"
1048 ),
1049 Self::CellContentAreaOverlapsCellPointers {
1050 cell_content_start,
1051 cell_pointer_array_end,
1052 } => write!(
1053 f,
1054 "cell content area overlaps cell pointer array: cell_content_start={cell_content_start} cell_pointer_array_end={cell_pointer_array_end}"
1055 ),
1056 Self::CellPointerArrayOutOfBounds {
1057 start,
1058 len,
1059 usable_size,
1060 } => write!(
1061 f,
1062 "cell pointer array out of bounds: start={start} len={len} usable_size={usable_size}"
1063 ),
1064 Self::InvalidCellPointer {
1065 index,
1066 offset,
1067 usable_size,
1068 } => write!(
1069 f,
1070 "invalid cell pointer: index={index} offset={offset} usable_size={usable_size}"
1071 ),
1072 Self::InvalidFreeblock {
1073 offset,
1074 size,
1075 usable_size,
1076 } => write!(
1077 f,
1078 "invalid freeblock: offset={offset} size={size} usable_size={usable_size}"
1079 ),
1080 Self::FreeblockLoop { offset } => write!(f, "freeblock loop at offset {offset}"),
1081 Self::InvalidRightMostChild { raw } => {
1082 write!(f, "invalid right-most child pointer: {raw}")
1083 }
1084 }
1085 }
1086}
1087
1088impl std::error::Error for BTreePageError {}
1089
1090#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1092pub struct BTreePageHeader {
1093 pub header_offset: usize,
1095 pub page_type: BTreePageType,
1097 pub first_freeblock: u16,
1099 pub cell_count: u16,
1101 pub cell_content_start: u32,
1103 pub fragmented_free_bytes: u8,
1105 pub right_most_child: Option<PageNumber>,
1107}
1108
1109impl BTreePageHeader {
1110 pub const fn header_size(self) -> usize {
1112 if self.page_type.is_leaf() { 8 } else { 12 }
1113 }
1114
1115 pub fn parse(
1117 page: &[u8],
1118 page_size: PageSize,
1119 reserved_per_page: u8,
1120 is_page1: bool,
1121 ) -> Result<Self, BTreePageError> {
1122 let expected = page_size.as_usize();
1123 if page.len() != expected {
1124 return Err(BTreePageError::PageSizeMismatch {
1125 expected,
1126 actual: page.len(),
1127 });
1128 }
1129
1130 let usable_size = page_size.usable(reserved_per_page) as usize;
1131 let header_offset = if is_page1 { DATABASE_HEADER_SIZE } else { 0 };
1132 let min_needed = header_offset + 8;
1133 if usable_size < min_needed {
1134 return Err(BTreePageError::PageTooSmall {
1135 usable_size,
1136 needed: min_needed,
1137 });
1138 }
1139
1140 let page_type_raw = page[header_offset];
1141 let page_type = BTreePageType::from_byte(page_type_raw)
1142 .ok_or(BTreePageError::InvalidPageType { raw: page_type_raw })?;
1143
1144 let header_size = if page_type.is_leaf() { 8 } else { 12 };
1145 let needed = header_offset + header_size;
1146 if usable_size < needed {
1147 return Err(BTreePageError::PageTooSmall {
1148 usable_size,
1149 needed,
1150 });
1151 }
1152
1153 let first_freeblock =
1154 u16::from_be_bytes([page[header_offset + 1], page[header_offset + 2]]);
1155 let cell_count = u16::from_be_bytes([page[header_offset + 3], page[header_offset + 4]]);
1156 let cell_content_raw =
1157 u16::from_be_bytes([page[header_offset + 5], page[header_offset + 6]]);
1158 let cell_content_start = if cell_content_raw == 0 {
1159 65_536
1160 } else {
1161 u32::from(cell_content_raw)
1162 };
1163 let usable_size_u32 = u32::try_from(usable_size).unwrap_or(u32::MAX);
1164 if cell_content_start > usable_size_u32 {
1165 return Err(BTreePageError::InvalidCellContentAreaStart {
1166 raw: cell_content_raw,
1167 decoded: cell_content_start,
1168 usable_size,
1169 });
1170 }
1171
1172 let fragmented_free_bytes = page[header_offset + 7];
1173 if fragmented_free_bytes > BTREE_MAX_FRAGMENTED_FREE_BYTES {
1174 return Err(BTreePageError::InvalidFragmentedFreeBytes {
1175 raw: fragmented_free_bytes,
1176 max: BTREE_MAX_FRAGMENTED_FREE_BYTES,
1177 });
1178 }
1179
1180 let right_most_child = if page_type.is_interior() {
1181 let raw = u32::from_be_bytes([
1182 page[header_offset + 8],
1183 page[header_offset + 9],
1184 page[header_offset + 10],
1185 page[header_offset + 11],
1186 ]);
1187 let pn = PageNumber::new(raw).ok_or(BTreePageError::InvalidRightMostChild { raw })?;
1188 Some(pn)
1189 } else {
1190 None
1191 };
1192
1193 let ptr_array_start = header_offset + header_size;
1195 let ptr_array_len = usize::from(cell_count) * 2;
1196 if ptr_array_start + ptr_array_len > usable_size {
1197 return Err(BTreePageError::CellPointerArrayOutOfBounds {
1198 start: ptr_array_start,
1199 len: ptr_array_len,
1200 usable_size,
1201 });
1202 }
1203 let ptr_array_end = ptr_array_start + ptr_array_len;
1204 let ptr_array_end_u32 = u32::try_from(ptr_array_end).unwrap_or(u32::MAX);
1205 if cell_content_start < ptr_array_end_u32 {
1206 return Err(BTreePageError::CellContentAreaOverlapsCellPointers {
1207 cell_content_start,
1208 cell_pointer_array_end: ptr_array_end,
1209 });
1210 }
1211
1212 Ok(Self {
1213 header_offset,
1214 page_type,
1215 first_freeblock,
1216 cell_count,
1217 cell_content_start,
1218 fragmented_free_bytes,
1219 right_most_child,
1220 })
1221 }
1222
1223 pub fn parse_cell_pointers(
1225 self,
1226 page: &[u8],
1227 page_size: PageSize,
1228 reserved_per_page: u8,
1229 ) -> Result<Vec<u16>, BTreePageError> {
1230 let expected = page_size.as_usize();
1231 if page.len() != expected {
1232 return Err(BTreePageError::PageSizeMismatch {
1233 expected,
1234 actual: page.len(),
1235 });
1236 }
1237
1238 let usable_size = page_size.usable(reserved_per_page) as usize;
1239 let ptr_array_start = self.header_offset + self.header_size();
1240 let ptr_array_len = usize::from(self.cell_count) * 2;
1241 if ptr_array_start + ptr_array_len > usable_size {
1242 return Err(BTreePageError::CellPointerArrayOutOfBounds {
1243 start: ptr_array_start,
1244 len: ptr_array_len,
1245 usable_size,
1246 });
1247 }
1248
1249 let min_cell_offset = ptr_array_start + ptr_array_len;
1250 let mut out = Vec::with_capacity(self.cell_count as usize);
1251 for i in 0..self.cell_count as usize {
1252 let off = ptr_array_start + i * 2;
1253 let cell_off = u16::from_be_bytes([page[off], page[off + 1]]);
1254 let cell_off_usize = usize::from(cell_off);
1255 if cell_off_usize < min_cell_offset
1256 || cell_off_usize < self.cell_content_start as usize
1257 || cell_off_usize >= usable_size
1258 {
1259 return Err(BTreePageError::InvalidCellPointer {
1260 index: i,
1261 offset: cell_off,
1262 usable_size,
1263 });
1264 }
1265 out.push(cell_off);
1266 }
1267 Ok(out)
1268 }
1269
1270 pub fn parse_freeblocks(
1272 self,
1273 page: &[u8],
1274 page_size: PageSize,
1275 reserved_per_page: u8,
1276 ) -> Result<Vec<Freeblock>, BTreePageError> {
1277 let expected = page_size.as_usize();
1278 if page.len() != expected {
1279 return Err(BTreePageError::PageSizeMismatch {
1280 expected,
1281 actual: page.len(),
1282 });
1283 }
1284 let usable_size = page_size.usable(reserved_per_page) as usize;
1285
1286 let mut blocks = Vec::new();
1287 let mut seen = std::collections::BTreeSet::new();
1288 let mut offset = self.first_freeblock;
1289 while offset != 0 {
1290 if !seen.insert(offset) {
1291 return Err(BTreePageError::FreeblockLoop { offset });
1292 }
1293
1294 let off = usize::from(offset);
1295 if off < self.cell_content_start as usize {
1296 return Err(BTreePageError::InvalidFreeblock {
1297 offset,
1298 size: 0,
1299 usable_size,
1300 });
1301 }
1302 if off + 4 > usable_size {
1303 return Err(BTreePageError::InvalidFreeblock {
1304 offset,
1305 size: 0,
1306 usable_size,
1307 });
1308 }
1309
1310 let next = u16::from_be_bytes([page[off], page[off + 1]]);
1311 let size = u16::from_be_bytes([page[off + 2], page[off + 3]]);
1312 if size < 4 || off + usize::from(size) > usable_size {
1313 return Err(BTreePageError::InvalidFreeblock {
1314 offset,
1315 size,
1316 usable_size,
1317 });
1318 }
1319
1320 blocks.push(Freeblock { offset, next, size });
1321 offset = next;
1322 }
1323
1324 Ok(blocks)
1325 }
1326
1327 #[allow(clippy::cast_possible_truncation)]
1341 pub fn write_empty_leaf_table(page: &mut [u8], header_offset: usize, usable_size: u32) {
1342 page[header_offset] = BTreePageType::LeafTable as u8; page[header_offset + 1] = 0;
1345 page[header_offset + 2] = 0;
1346 page[header_offset + 3] = 0;
1348 page[header_offset + 4] = 0;
1349 let content_raw = if usable_size >= 65_536 {
1351 0u16
1352 } else {
1353 usable_size as u16
1354 };
1355 page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1356 page[header_offset + 7] = 0;
1358 }
1359
1360 #[allow(clippy::cast_possible_truncation)]
1368 pub fn write_empty_leaf_index(page: &mut [u8], header_offset: usize, usable_size: u32) {
1369 page[header_offset] = BTreePageType::LeafIndex as u8; page[header_offset + 1] = 0;
1372 page[header_offset + 2] = 0;
1373 page[header_offset + 3] = 0;
1375 page[header_offset + 4] = 0;
1376 let content_raw = if usable_size >= 65_536 {
1378 0u16
1379 } else {
1380 usable_size as u16
1381 };
1382 page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1383 page[header_offset + 7] = 0;
1385 }
1386}
1387
1388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1390pub struct Freeblock {
1391 pub offset: u16,
1392 pub next: u16,
1393 pub size: u16,
1394}
1395
1396pub const fn would_exceed_fragmented_free_bytes(current: u8, additional: u8) -> bool {
1398 current.saturating_add(additional) > BTREE_MAX_FRAGMENTED_FREE_BYTES
1399}
1400
1401#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1403#[repr(u8)]
1404pub enum BTreePageType {
1405 InteriorIndex = 2,
1407 InteriorTable = 5,
1409 LeafIndex = 10,
1411 LeafTable = 13,
1413}
1414
1415impl BTreePageType {
1416 pub const fn from_byte(b: u8) -> Option<Self> {
1418 match b {
1419 2 => Some(Self::InteriorIndex),
1420 5 => Some(Self::InteriorTable),
1421 10 => Some(Self::LeafIndex),
1422 13 => Some(Self::LeafTable),
1423 _ => None,
1424 }
1425 }
1426
1427 pub const fn is_leaf(self) -> bool {
1429 matches!(self, Self::LeafIndex | Self::LeafTable)
1430 }
1431
1432 pub const fn is_interior(self) -> bool {
1434 matches!(self, Self::InteriorIndex | Self::InteriorTable)
1435 }
1436
1437 pub const fn is_table(self) -> bool {
1439 matches!(self, Self::InteriorTable | Self::LeafTable)
1440 }
1441
1442 pub const fn is_index(self) -> bool {
1444 matches!(self, Self::InteriorIndex | Self::LeafIndex)
1445 }
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450 use super::*;
1451
1452 #[test]
1453 fn page_number_zero_is_invalid() {
1454 assert!(PageNumber::new(0).is_none());
1455 assert!(PageNumber::try_from(0u32).is_err());
1456 }
1457
1458 #[test]
1459 fn test_page_number_zero_rejected() {
1460 assert!(PageNumber::new(0).is_none());
1461 assert!(PageNumber::try_from(0u32).is_err());
1462 }
1463
1464 #[test]
1465 fn page_number_valid() {
1466 let pn = PageNumber::new(1).unwrap();
1467 assert_eq!(pn.get(), 1);
1468 assert_eq!(pn, PageNumber::ONE);
1469
1470 let pn = PageNumber::new(42).unwrap();
1471 assert_eq!(pn.get(), 42);
1472 assert_eq!(pn.to_string(), "42");
1473 }
1474
1475 #[test]
1476 fn page_number_ordering() {
1477 let a = PageNumber::new(1).unwrap();
1478 let b = PageNumber::new(100).unwrap();
1479 assert!(a < b);
1480 }
1481
1482 #[test]
1483 fn page_size_validation() {
1484 assert!(PageSize::new(0).is_none());
1485 assert!(PageSize::new(256).is_none());
1486 assert!(PageSize::new(511).is_none());
1487 assert!(PageSize::new(513).is_none());
1488 assert!(PageSize::new(1000).is_none());
1489 assert!(PageSize::new(131_072).is_none());
1490
1491 assert!(PageSize::new(512).is_some());
1492 assert!(PageSize::new(1024).is_some());
1493 assert!(PageSize::new(4096).is_some());
1494 assert!(PageSize::new(8192).is_some());
1495 assert!(PageSize::new(16384).is_some());
1496 assert!(PageSize::new(32768).is_some());
1497 assert!(PageSize::new(65536).is_some());
1498 }
1499
1500 #[test]
1501 fn page_size_defaults() {
1502 assert_eq!(PageSize::DEFAULT.get(), 4096);
1503 assert_eq!(PageSize::MIN.get(), 512);
1504 assert_eq!(PageSize::MAX.get(), 65536);
1505 assert_eq!(PageSize::default(), PageSize::DEFAULT);
1506 }
1507
1508 fn make_header_for_tests() -> DatabaseHeader {
1509 DatabaseHeader {
1510 page_size: PageSize::DEFAULT,
1511 write_version: 2,
1512 read_version: 2,
1513 reserved_per_page: 0,
1514 change_counter: 7,
1515 page_count: 123,
1516 freelist_trunk: 0,
1517 freelist_count: 0,
1518 schema_cookie: 1,
1519 schema_format: 4,
1520 default_cache_size: -2000,
1521 largest_root_page: 0,
1522 text_encoding: TextEncoding::Utf8,
1523 user_version: 0,
1524 incremental_vacuum: 0,
1525 application_id: 0,
1526 version_valid_for: 7,
1527 sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
1528 }
1529 }
1530
1531 #[test]
1532 fn test_header_magic_validation() {
1533 let hdr = make_header_for_tests();
1534 let mut buf = hdr.to_bytes().unwrap();
1535 let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
1536 assert_eq!(parsed, hdr);
1537
1538 buf[0] = b'X';
1539 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1540 assert!(matches!(err, DatabaseHeaderError::InvalidMagic));
1541 }
1542
1543 #[test]
1544 fn test_header_page_size_encoding() {
1545 let mut hdr = make_header_for_tests();
1547 hdr.page_size = PageSize::new(65_536).unwrap();
1548 let buf = hdr.to_bytes().unwrap();
1549 assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), 1);
1550 assert_eq!(
1551 DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1552 65_536
1553 );
1554
1555 for size in [512u32, 1024, 2048, 4096, 8192, 16_384, 32_768] {
1557 hdr.page_size = PageSize::new(size).unwrap();
1558 let buf = hdr.to_bytes().unwrap();
1559 let expected_u16 = u16::try_from(size).unwrap();
1560 assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), expected_u16);
1561 assert_eq!(
1562 DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1563 size
1564 );
1565 }
1566
1567 let mut buf = make_header_for_tests().to_bytes().unwrap();
1569 buf[16..18].copy_from_slice(&1000u16.to_be_bytes());
1570 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1571 assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1572 }
1573
1574 #[test]
1575 fn test_header_page_size_range() {
1576 let mut buf = make_header_for_tests().to_bytes().unwrap();
1577 buf[16..18].copy_from_slice(&256u16.to_be_bytes());
1578 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1579 assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1580 }
1581
1582 #[test]
1583 fn test_header_write_read_version() {
1584 let mut hdr = make_header_for_tests();
1585
1586 hdr.write_version = 2;
1587 hdr.read_version = 2;
1588 assert_eq!(
1589 hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1590 DatabaseOpenMode::ReadWrite
1591 );
1592
1593 hdr.read_version = 3;
1594 let err = hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap_err();
1595 assert!(matches!(
1596 err,
1597 DatabaseHeaderError::UnsupportedReadVersion { .. }
1598 ));
1599
1600 hdr.read_version = 2;
1601 hdr.write_version = 3;
1602 assert_eq!(
1603 hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1604 DatabaseOpenMode::ReadOnly
1605 );
1606 }
1607
1608 #[test]
1609 fn test_header_payload_fractions() {
1610 let mut buf = make_header_for_tests().to_bytes().unwrap();
1611 buf[21] = 65;
1612 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1613 assert!(matches!(
1614 err,
1615 DatabaseHeaderError::InvalidPayloadFractions { .. }
1616 ));
1617 }
1618
1619 #[test]
1620 fn test_header_usable_size_minimum() {
1621 let mut buf = make_header_for_tests().to_bytes().unwrap();
1623 buf[16..18].copy_from_slice(&512u16.to_be_bytes());
1624 buf[20] = 33;
1625 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1626 assert!(matches!(
1627 err,
1628 DatabaseHeaderError::UsableSizeTooSmall { .. }
1629 ));
1630
1631 buf[20] = 32;
1632 DatabaseHeader::from_bytes(&buf).unwrap();
1633 }
1634
1635 #[test]
1636 fn test_header_round_trip() {
1637 let hdr = make_header_for_tests();
1638 let buf1 = hdr.to_bytes().unwrap();
1639 let parsed = DatabaseHeader::from_bytes(&buf1).unwrap();
1640 assert_eq!(parsed, hdr);
1641
1642 let buf2 = parsed.to_bytes().unwrap();
1643 assert_eq!(buf1, buf2);
1644 }
1645
1646 #[test]
1647 fn test_btree_page_header_leaf() {
1648 let page_size = PageSize::new(512).unwrap();
1649 let mut page = vec![0u8; page_size.as_usize()];
1650
1651 page[0] = 0x0D;
1653 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();
1659 assert!(hdr.page_type.is_leaf());
1660 assert_eq!(hdr.header_size(), 8);
1661 }
1662
1663 #[test]
1664 fn test_btree_page_header_interior() {
1665 let page_size = PageSize::new(512).unwrap();
1666 let mut page = vec![0u8; page_size.as_usize()];
1667
1668 page[0] = 0x05;
1670 page[1..3].copy_from_slice(&0u16.to_be_bytes());
1671 page[3..5].copy_from_slice(&0u16.to_be_bytes());
1672 page[5..7].copy_from_slice(&500u16.to_be_bytes());
1673 page[7] = 0;
1674 page[8..12].copy_from_slice(&2u32.to_be_bytes()); let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1677 assert!(hdr.page_type.is_interior());
1678 assert_eq!(hdr.header_size(), 12);
1679 assert_eq!(hdr.right_most_child.unwrap().get(), 2);
1680 }
1681
1682 #[test]
1683 fn test_page1_offset_adjustment() {
1684 let page_size = PageSize::new(512).unwrap();
1685 let mut page = vec![0u8; page_size.as_usize()];
1686
1687 let h = DATABASE_HEADER_SIZE;
1689 page[h] = 0x0D; page[h + 1..h + 3].copy_from_slice(&0u16.to_be_bytes());
1691 page[h + 3..h + 5].copy_from_slice(&1u16.to_be_bytes()); page[h + 5..h + 7].copy_from_slice(&300u16.to_be_bytes()); page[h + 7] = 0;
1694
1695 page[h + 8..h + 10].copy_from_slice(&300u16.to_be_bytes());
1697
1698 let hdr = BTreePageHeader::parse(&page, page_size, 0, true).unwrap();
1699 let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
1700 assert_eq!(ptrs, vec![300u16]);
1701 }
1702
1703 #[test]
1704 fn test_cell_pointer_array() {
1705 let page_size = PageSize::new(512).unwrap();
1706 let mut page = vec![0u8; page_size.as_usize()];
1707
1708 page[0] = 0x0D;
1709 page[1..3].copy_from_slice(&0u16.to_be_bytes());
1710 page[3..5].copy_from_slice(&3u16.to_be_bytes()); page[5..7].copy_from_slice(&300u16.to_be_bytes());
1712 page[7] = 0;
1713 page[8..10].copy_from_slice(&300u16.to_be_bytes());
1714 page[10..12].copy_from_slice(&320u16.to_be_bytes());
1715 page[12..14].copy_from_slice(&340u16.to_be_bytes());
1716
1717 let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1718 let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
1719 assert_eq!(ptrs, vec![300u16, 320u16, 340u16]);
1720 }
1721
1722 #[test]
1723 fn test_freeblock_list_traversal() {
1724 let page_size = PageSize::new(512).unwrap();
1725 let mut page = vec![0u8; page_size.as_usize()];
1726
1727 page[0] = 0x0D;
1728 page[1..3].copy_from_slice(&400u16.to_be_bytes()); page[3..5].copy_from_slice(&0u16.to_be_bytes());
1730 page[5..7].copy_from_slice(&400u16.to_be_bytes());
1731 page[7] = 0;
1732
1733 page[400..402].copy_from_slice(&420u16.to_be_bytes());
1735 page[402..404].copy_from_slice(&20u16.to_be_bytes());
1736 page[420..422].copy_from_slice(&0u16.to_be_bytes());
1738 page[422..424].copy_from_slice(&30u16.to_be_bytes());
1739
1740 let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1741 let blocks = hdr.parse_freeblocks(&page, page_size, 0).unwrap();
1742 assert_eq!(
1743 blocks,
1744 vec![
1745 Freeblock {
1746 offset: 400,
1747 next: 420,
1748 size: 20
1749 },
1750 Freeblock {
1751 offset: 420,
1752 next: 0,
1753 size: 30
1754 }
1755 ]
1756 );
1757 }
1758
1759 #[test]
1760 fn test_freeblock_min_size() {
1761 let page_size = PageSize::new(512).unwrap();
1762 let mut page = vec![0u8; page_size.as_usize()];
1763
1764 page[0] = 0x0D;
1765 page[1..3].copy_from_slice(&400u16.to_be_bytes());
1766 page[3..5].copy_from_slice(&0u16.to_be_bytes());
1767 page[5..7].copy_from_slice(&400u16.to_be_bytes());
1768 page[7] = 0;
1769
1770 page[400..402].copy_from_slice(&0u16.to_be_bytes());
1771 page[402..404].copy_from_slice(&3u16.to_be_bytes()); let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1774 let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
1775 assert!(matches!(err, BTreePageError::InvalidFreeblock { .. }));
1776 }
1777
1778 #[test]
1779 fn test_fragment_defrag_threshold() {
1780 assert!(!would_exceed_fragmented_free_bytes(60, 0));
1781 assert!(would_exceed_fragmented_free_bytes(60, 1));
1782 assert!(would_exceed_fragmented_free_bytes(59, 2));
1783 }
1784
1785 #[test]
1786 fn test_e2e_bd_1a32() {
1787 use std::fs::File;
1788 use std::io::{Read, Seek};
1789 use std::process::Command;
1790 use std::sync::atomic::{AtomicUsize, Ordering};
1791
1792 static COUNTER: AtomicUsize = AtomicUsize::new(0);
1793
1794 if Command::new("sqlite3").arg("--version").output().is_err() {
1796 return;
1797 }
1798
1799 let mut path = std::env::temp_dir();
1800 path.push(format!(
1801 "fsqlite_bd_1a32_{}_{}.sqlite",
1802 std::process::id(),
1803 COUNTER.fetch_add(1, Ordering::Relaxed)
1804 ));
1805
1806 let status = Command::new("sqlite3")
1807 .arg(&path)
1808 .arg("CREATE TABLE t(x); INSERT INTO t VALUES(1);")
1809 .status()
1810 .expect("sqlite3 execution failed");
1811 assert!(status.success());
1812
1813 let mut f = File::open(&path).expect("open temp db");
1814 let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
1815 f.read_exact(&mut header_bytes).expect("read db header");
1816 let header = DatabaseHeader::from_bytes(&header_bytes).expect("parse db header");
1817 assert_eq!(header.schema_format, 4);
1818 assert_eq!(
1819 header.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1820 DatabaseOpenMode::ReadWrite
1821 );
1822
1823 let hdr2 = header.to_bytes().expect("serialize header");
1825 assert_eq!(header_bytes, hdr2);
1826
1827 let page_size = header.page_size;
1829 let mut page1 = vec![0u8; page_size.as_usize()];
1830 f.rewind().expect("rewind");
1831 f.read_exact(&mut page1).expect("read page 1");
1832 let btree_hdr = BTreePageHeader::parse(&page1, page_size, header.reserved_per_page, true)
1833 .expect("parse page1 btree header");
1834 assert_eq!(btree_hdr.header_offset, DATABASE_HEADER_SIZE);
1835 }
1836
1837 #[test]
1838 fn test_varint_signed_cast() {
1839 use crate::serial_type::{read_varint, write_varint};
1840
1841 let test_cases: &[(u64, i64)] = &[
1843 (0, 0),
1844 (1, 1),
1845 (0x7FFF_FFFF_FFFF_FFFF, i64::MAX),
1846 (u64::MAX, -1),
1847 (0x8000_0000_0000_0000, i64::MIN),
1848 ];
1849 let mut buf = [0u8; 9];
1850 for &(unsigned, expected_signed) in test_cases {
1851 let written = write_varint(&mut buf, unsigned);
1852 let (decoded, consumed) = read_varint(&buf[..written]).unwrap();
1853 assert_eq!(decoded, unsigned);
1854 assert_eq!(consumed, written);
1855 #[allow(clippy::cast_possible_wrap)]
1856 let signed = decoded as i64;
1857 assert_eq!(
1858 signed, expected_signed,
1859 "u64 {unsigned} should cast to i64 {expected_signed}, got {signed}"
1860 );
1861 }
1862 }
1863
1864 #[test]
1865 fn test_reserved_bytes_72_91_zero() {
1866 let hdr = make_header_for_tests();
1867 let buf = hdr.to_bytes().unwrap();
1868 for (i, &byte) in buf.iter().enumerate().take(92).skip(72) {
1869 assert_eq!(byte, 0, "byte {i} should be zero (reserved region)");
1870 }
1871
1872 let mut hdr2 = make_header_for_tests();
1873 hdr2.application_id = 0xDEAD_BEEF;
1874 hdr2.user_version = 42;
1875 let buf2 = hdr2.to_bytes().unwrap();
1876 for (i, &byte) in buf2.iter().enumerate().take(92).skip(72) {
1877 assert_eq!(byte, 0, "byte {i} should be zero even with custom app_id");
1878 }
1879 }
1880
1881 #[test]
1882 fn test_version_valid_for_stale() {
1883 let mut hdr = make_header_for_tests();
1884 hdr.change_counter = 7;
1885 hdr.version_valid_for = 7;
1886 assert!(!hdr.is_page_count_stale());
1887
1888 hdr.version_valid_for = 5;
1889 assert!(hdr.is_page_count_stale());
1890
1891 hdr.page_size = PageSize::new(4096).unwrap();
1892 assert_eq!(hdr.page_count_from_file_size(4096 * 100), Some(100));
1893 assert_eq!(hdr.page_count_from_file_size(4096), Some(1));
1894 assert!(hdr.page_count_from_file_size(5000).is_none());
1895 assert!(hdr.page_count_from_file_size(0).is_none());
1896 }
1897
1898 #[test]
1899 fn test_reserved_space_per_page() {
1900 let mut hdr = make_header_for_tests();
1901 hdr.page_size = PageSize::new(4096).unwrap();
1902 hdr.reserved_per_page = 40;
1903 let usable = hdr.page_size.usable(hdr.reserved_per_page);
1904 assert_eq!(usable, 4056);
1905
1906 let buf = hdr.to_bytes().unwrap();
1907 let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
1908 assert_eq!(parsed.reserved_per_page, 40);
1909 assert_eq!(parsed.page_size.usable(parsed.reserved_per_page), 4056);
1910 }
1911
1912 #[test]
1913 fn test_header_text_encoding_invalid() {
1914 let mut buf = make_header_for_tests().to_bytes().unwrap();
1915 buf[56..60].copy_from_slice(&4u32.to_be_bytes());
1916 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1917 assert!(matches!(
1918 err,
1919 DatabaseHeaderError::InvalidTextEncoding { raw: 4 }
1920 ));
1921
1922 buf[56..60].copy_from_slice(&0u32.to_be_bytes());
1923 let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1924 assert!(matches!(
1925 err,
1926 DatabaseHeaderError::InvalidTextEncoding { raw: 0 }
1927 ));
1928 }
1929
1930 #[test]
1931 fn test_btree_page_type_classification() {
1932 assert_eq!(
1933 BTreePageType::from_byte(0x02),
1934 Some(BTreePageType::InteriorIndex)
1935 );
1936 assert_eq!(
1937 BTreePageType::from_byte(0x05),
1938 Some(BTreePageType::InteriorTable)
1939 );
1940 assert_eq!(
1941 BTreePageType::from_byte(0x0A),
1942 Some(BTreePageType::LeafIndex)
1943 );
1944 assert_eq!(
1945 BTreePageType::from_byte(0x0D),
1946 Some(BTreePageType::LeafTable)
1947 );
1948
1949 assert!(BTreePageType::from_byte(0x00).is_none());
1950 assert!(BTreePageType::from_byte(0x01).is_none());
1951 assert!(BTreePageType::from_byte(0xFF).is_none());
1952
1953 assert!(BTreePageType::InteriorTable.is_interior());
1954 assert!(BTreePageType::InteriorTable.is_table());
1955 assert!(!BTreePageType::InteriorTable.is_leaf());
1956 assert!(!BTreePageType::InteriorTable.is_index());
1957
1958 assert!(BTreePageType::LeafIndex.is_leaf());
1959 assert!(BTreePageType::LeafIndex.is_index());
1960 assert!(!BTreePageType::LeafIndex.is_interior());
1961 assert!(!BTreePageType::LeafIndex.is_table());
1962 }
1963
1964 #[test]
1965 fn test_invalid_page_type_rejected() {
1966 let page_size = PageSize::new(512).unwrap();
1967 let mut page = vec![0u8; page_size.as_usize()];
1968 page[0] = 0x01;
1969 let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
1970 assert!(matches!(err, BTreePageError::InvalidPageType { raw: 0x01 }));
1971 }
1972
1973 #[test]
1974 fn test_freeblock_loop_detected() {
1975 let page_size = PageSize::new(512).unwrap();
1976 let mut page = vec![0u8; page_size.as_usize()];
1977
1978 page[0] = 0x0D;
1979 page[1..3].copy_from_slice(&400u16.to_be_bytes()); page[3..5].copy_from_slice(&0u16.to_be_bytes()); page[5..7].copy_from_slice(&300u16.to_be_bytes());
1983 page[7] = 0;
1984
1985 page[400..402].copy_from_slice(&420u16.to_be_bytes());
1987 page[402..404].copy_from_slice(&20u16.to_be_bytes());
1988 page[420..422].copy_from_slice(&400u16.to_be_bytes());
1990 page[422..424].copy_from_slice(&20u16.to_be_bytes());
1991
1992 let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
1993 let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
1994 assert!(matches!(err, BTreePageError::FreeblockLoop { .. }));
1995 }
1996
1997 #[test]
1998 fn test_fragmented_free_bytes_max() {
1999 let page_size = PageSize::new(512).unwrap();
2000 let mut page = vec![0u8; page_size.as_usize()];
2001
2002 page[0] = 0x0D;
2003 page[5..7].copy_from_slice(&500u16.to_be_bytes()); page[7] = 61; let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2006 assert!(matches!(
2007 err,
2008 BTreePageError::InvalidFragmentedFreeBytes { raw: 61, max: 60 }
2009 ));
2010
2011 page[7] = 60;
2013 BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2014 }
2015
2016 #[test]
2017 fn test_error_variants_distinct_display() {
2018 let errors: Vec<DatabaseHeaderError> = vec![
2019 DatabaseHeaderError::InvalidMagic,
2020 DatabaseHeaderError::InvalidPageSize { raw: 100 },
2021 DatabaseHeaderError::InvalidPayloadFractions {
2022 max: 65,
2023 min: 32,
2024 leaf: 32,
2025 },
2026 DatabaseHeaderError::UsableSizeTooSmall {
2027 page_size: 512,
2028 reserved_per_page: 33,
2029 usable_size: 479,
2030 },
2031 DatabaseHeaderError::UnsupportedReadVersion {
2032 read_version: 3,
2033 max_supported: 2,
2034 },
2035 DatabaseHeaderError::InvalidTextEncoding { raw: 4 },
2036 DatabaseHeaderError::InvalidSchemaFormat { raw: 0 },
2037 ];
2038
2039 let displays: Vec<String> = errors
2040 .iter()
2041 .map(std::string::ToString::to_string)
2042 .collect();
2043 for (i, d) in displays.iter().enumerate() {
2044 assert!(!d.is_empty(), "error variant {i} has empty display");
2045 for (j, d2) in displays.iter().enumerate() {
2046 if i != j {
2047 assert_ne!(d, d2, "error variants {i} and {j} have identical display");
2048 }
2049 }
2050 }
2051 }
2052
2053 #[test]
2056 fn test_sqlite_master_page1_root() {
2057 let page_size = PageSize::new(4096).unwrap();
2060 let mut page = [0u8; 4096];
2061 page[..16].copy_from_slice(b"SQLite format 3\0");
2064 page[16..18].copy_from_slice(&4096u16.to_be_bytes()); page[100] = 0x0D; page[103..105].copy_from_slice(&0u16.to_be_bytes());
2068 page[105..107].copy_from_slice(&4096u16.to_be_bytes()); let page_type = BTreePageType::from_byte(page[100]);
2072 assert_eq!(page_type, Some(BTreePageType::LeafTable));
2073 let hdr = BTreePageHeader::parse(&page, page_size, 0, true).expect("valid leaf header");
2074 assert_eq!(hdr.cell_count, 0, "fresh sqlite_master has 0 rows");
2075 }
2076
2077 #[test]
2078 fn test_sqlite_master_schema_columns() {
2079 let columns = ["type", "name", "tbl_name", "rootpage", "sql"];
2081 assert_eq!(columns.len(), 5);
2082 let valid_types = ["table", "index", "view", "trigger"];
2084 assert_eq!(valid_types.len(), 4);
2085 }
2086
2087 #[test]
2088 fn test_encoding_utf8_default() {
2089 let hdr = DatabaseHeader::default();
2091 assert_eq!(hdr.text_encoding, TextEncoding::Utf8);
2092
2093 let bytes = hdr.to_bytes().expect("encode");
2094 let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2096 assert_eq!(enc_raw, 1, "UTF-8 encoding stored as 1 at offset 56");
2097 }
2098
2099 #[test]
2100 fn test_encoding_utf16le() {
2101 let mut hdr = make_header_for_tests();
2102 hdr.text_encoding = TextEncoding::Utf16le;
2103 let bytes = hdr.to_bytes().expect("encode");
2104 let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2105 assert_eq!(enc_raw, 2, "UTF-16LE encoding stored as 2");
2106
2107 let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2108 assert_eq!(parsed.text_encoding, TextEncoding::Utf16le);
2109 }
2110
2111 #[test]
2112 fn test_encoding_utf16be() {
2113 let mut hdr = make_header_for_tests();
2114 hdr.text_encoding = TextEncoding::Utf16be;
2115 let bytes = hdr.to_bytes().expect("encode");
2116 let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2117 assert_eq!(enc_raw, 3, "UTF-16BE encoding stored as 3");
2118
2119 let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2120 assert_eq!(parsed.text_encoding, TextEncoding::Utf16be);
2121 }
2122
2123 #[test]
2124 fn test_encoding_immutable_after_creation() {
2125 let hdr1 = make_header_for_tests();
2130 assert_eq!(hdr1.text_encoding, TextEncoding::Utf8);
2131 let bytes1 = hdr1.to_bytes().expect("encode");
2132
2133 let mut hdr2 = hdr1;
2134 hdr2.text_encoding = TextEncoding::Utf16le;
2135 let bytes2 = hdr2.to_bytes().expect("encode");
2136
2137 assert_ne!(
2139 bytes1[56..60],
2140 bytes2[56..60],
2141 "different encodings must serialize differently"
2142 );
2143 }
2144
2145 #[test]
2146 fn test_binary_collation_memcmp_utf8() {
2147 let a = "abc";
2150 let b = "abd";
2151 assert!(
2152 a.as_bytes() < b.as_bytes(),
2153 "memcmp ordering for ASCII UTF-8"
2154 );
2155
2156 let z = "z";
2160 let e_acute = "é";
2161 assert!(
2162 z.as_bytes() < e_acute.as_bytes(),
2163 "UTF-8 memcmp preserves code point order"
2164 );
2165 }
2166
2167 #[test]
2170 fn test_affinity_int_keyword() {
2171 assert_eq!(
2172 TypeAffinity::from_type_name("INTEGER"),
2173 TypeAffinity::Integer
2174 );
2175 assert_eq!(TypeAffinity::from_type_name("INT"), TypeAffinity::Integer);
2176 assert_eq!(
2177 TypeAffinity::from_type_name("TINYINT"),
2178 TypeAffinity::Integer
2179 );
2180 assert_eq!(
2181 TypeAffinity::from_type_name("SMALLINT"),
2182 TypeAffinity::Integer
2183 );
2184 assert_eq!(
2185 TypeAffinity::from_type_name("MEDIUMINT"),
2186 TypeAffinity::Integer
2187 );
2188 assert_eq!(
2189 TypeAffinity::from_type_name("BIGINT"),
2190 TypeAffinity::Integer
2191 );
2192 assert_eq!(
2193 TypeAffinity::from_type_name("UNSIGNED BIG INT"),
2194 TypeAffinity::Integer
2195 );
2196 assert_eq!(TypeAffinity::from_type_name("INT2"), TypeAffinity::Integer);
2197 assert_eq!(TypeAffinity::from_type_name("INT8"), TypeAffinity::Integer);
2198 }
2199
2200 #[test]
2201 fn test_affinity_text_keyword() {
2202 assert_eq!(TypeAffinity::from_type_name("TEXT"), TypeAffinity::Text);
2203 assert_eq!(
2204 TypeAffinity::from_type_name("CHARACTER(20)"),
2205 TypeAffinity::Text
2206 );
2207 assert_eq!(
2208 TypeAffinity::from_type_name("VARCHAR(255)"),
2209 TypeAffinity::Text
2210 );
2211 assert_eq!(
2212 TypeAffinity::from_type_name("VARYING CHARACTER(255)"),
2213 TypeAffinity::Text
2214 );
2215 assert_eq!(
2216 TypeAffinity::from_type_name("NCHAR(55)"),
2217 TypeAffinity::Text
2218 );
2219 assert_eq!(
2220 TypeAffinity::from_type_name("NATIVE CHARACTER(70)"),
2221 TypeAffinity::Text
2222 );
2223 assert_eq!(
2224 TypeAffinity::from_type_name("NVARCHAR(100)"),
2225 TypeAffinity::Text
2226 );
2227 assert_eq!(TypeAffinity::from_type_name("CLOB"), TypeAffinity::Text);
2228 }
2229
2230 #[test]
2231 fn test_affinity_blob_keyword() {
2232 assert_eq!(TypeAffinity::from_type_name("BLOB"), TypeAffinity::Blob);
2233 assert_eq!(TypeAffinity::from_type_name("blob"), TypeAffinity::Blob);
2234 }
2235
2236 #[test]
2237 fn test_affinity_empty_type() {
2238 assert_eq!(TypeAffinity::from_type_name(""), TypeAffinity::Blob);
2239 }
2240
2241 #[test]
2242 fn test_affinity_real_keyword() {
2243 assert_eq!(TypeAffinity::from_type_name("REAL"), TypeAffinity::Real);
2244 assert_eq!(TypeAffinity::from_type_name("DOUBLE"), TypeAffinity::Real);
2245 assert_eq!(
2246 TypeAffinity::from_type_name("DOUBLE PRECISION"),
2247 TypeAffinity::Real
2248 );
2249 assert_eq!(TypeAffinity::from_type_name("FLOAT"), TypeAffinity::Real);
2250 }
2251
2252 #[test]
2253 fn test_affinity_numeric_keyword() {
2254 assert_eq!(
2255 TypeAffinity::from_type_name("NUMERIC"),
2256 TypeAffinity::Numeric
2257 );
2258 assert_eq!(
2259 TypeAffinity::from_type_name("DECIMAL(10,5)"),
2260 TypeAffinity::Numeric
2261 );
2262 assert_eq!(
2263 TypeAffinity::from_type_name("BOOLEAN"),
2264 TypeAffinity::Numeric
2265 );
2266 assert_eq!(TypeAffinity::from_type_name("DATE"), TypeAffinity::Numeric);
2267 assert_eq!(
2268 TypeAffinity::from_type_name("DATETIME"),
2269 TypeAffinity::Numeric
2270 );
2271 }
2272
2273 #[test]
2274 fn test_affinity_case_insensitive() {
2275 assert_eq!(
2276 TypeAffinity::from_type_name("integer"),
2277 TypeAffinity::Integer
2278 );
2279 assert_eq!(TypeAffinity::from_type_name("text"), TypeAffinity::Text);
2280 assert_eq!(TypeAffinity::from_type_name("Real"), TypeAffinity::Real);
2281 assert_eq!(
2282 TypeAffinity::from_type_name("Numeric"),
2283 TypeAffinity::Numeric
2284 );
2285 }
2286
2287 #[test]
2288 fn test_affinity_first_match_int_before_char() {
2289 assert_eq!(
2291 TypeAffinity::from_type_name("CHARINT"),
2292 TypeAffinity::Integer
2293 );
2294 assert_eq!(
2296 TypeAffinity::from_type_name("POINTERFLOAT"),
2297 TypeAffinity::Integer
2298 );
2299 }
2300
2301 #[test]
2302 fn test_comparison_numeric_vs_text() {
2303 assert_eq!(
2304 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Text),
2305 Some(TypeAffinity::Numeric)
2306 );
2307 assert_eq!(
2308 TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Real),
2309 Some(TypeAffinity::Numeric)
2310 );
2311 assert_eq!(
2312 TypeAffinity::comparison_affinity(TypeAffinity::Numeric, TypeAffinity::Blob),
2313 Some(TypeAffinity::Numeric)
2314 );
2315 }
2316
2317 #[test]
2318 fn test_comparison_text_vs_blob() {
2319 assert_eq!(
2320 TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Blob),
2321 Some(TypeAffinity::Text)
2322 );
2323 assert_eq!(
2324 TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Text),
2325 Some(TypeAffinity::Text)
2326 );
2327 }
2328
2329 #[test]
2330 fn test_comparison_same_affinity_no_coercion() {
2331 assert_eq!(
2332 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Integer),
2333 None
2334 );
2335 assert_eq!(
2336 TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Text),
2337 None
2338 );
2339 assert_eq!(
2340 TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2341 None
2342 );
2343 }
2344
2345 #[test]
2346 fn test_comparison_both_blob_no_coercion() {
2347 assert_eq!(
2348 TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2349 None
2350 );
2351 }
2352
2353 #[test]
2354 fn test_affinity_applied_to_needing_operand_only() {
2355 let left = SqliteValue::Integer(42);
2356 let right = SqliteValue::Text("123".to_string());
2357 let affinity = TypeAffinity::comparison_affinity(left.affinity(), right.affinity())
2358 .expect("numeric-vs-text comparison must request numeric coercion");
2359
2360 let left_after = left.clone();
2362 let right_after = right.apply_affinity(affinity);
2364
2365 assert_eq!(left_after, left);
2366 assert_eq!(right_after, SqliteValue::Integer(123));
2367 }
2368
2369 #[test]
2370 fn test_comparison_numeric_subtypes() {
2371 assert_eq!(
2374 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Real),
2375 None
2376 );
2377 assert_eq!(
2378 TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Numeric),
2379 None
2380 );
2381 assert_eq!(
2382 TypeAffinity::comparison_affinity(TypeAffinity::Real, TypeAffinity::Numeric),
2383 None
2384 );
2385 }
2386
2387 #[test]
2390 fn test_write_empty_leaf_table_basic() {
2391 let ps = PageSize::DEFAULT;
2392 let mut buf = vec![0u8; ps.as_usize()];
2393 BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2394
2395 assert_eq!(buf[0], 0x0D, "page type LeafTable");
2396 assert_eq!(buf[1], 0, "first_freeblock hi");
2397 assert_eq!(buf[2], 0, "first_freeblock lo");
2398 assert_eq!(buf[3], 0, "cell_count hi");
2399 assert_eq!(buf[4], 0, "cell_count lo");
2400 assert_eq!(buf[5], 0x10, "content_offset hi");
2402 assert_eq!(buf[6], 0x00, "content_offset lo");
2403 assert_eq!(buf[7], 0, "fragmented_free_bytes");
2404 }
2405
2406 #[test]
2407 fn test_write_empty_leaf_table_page1_offset() {
2408 let ps = PageSize::DEFAULT;
2409 let mut buf = vec![0u8; ps.as_usize()];
2410 BTreePageHeader::write_empty_leaf_table(&mut buf, DATABASE_HEADER_SIZE, ps.get());
2411
2412 assert_eq!(buf[DATABASE_HEADER_SIZE], 0x0D, "page type at offset 100");
2413 assert!(buf[..DATABASE_HEADER_SIZE].iter().all(|&b| b == 0));
2415 }
2416
2417 #[test]
2418 fn test_write_empty_leaf_table_65536_encoding() {
2419 let ps = PageSize::new(65536).unwrap();
2420 let mut buf = vec![0u8; ps.as_usize()];
2421 BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2422
2423 assert_eq!(buf[5], 0x00, "65536 encoded as 0 hi");
2425 assert_eq!(buf[6], 0x00, "65536 encoded as 0 lo");
2426 }
2427
2428 #[test]
2429 fn test_write_empty_leaf_table_512_page_size() {
2430 let ps = PageSize::new(512).unwrap();
2431 let mut buf = vec![0u8; ps.as_usize()];
2432 BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2433
2434 assert_eq!(buf[5], 0x02, "512 hi byte");
2436 assert_eq!(buf[6], 0x00, "512 lo byte");
2437 }
2438}