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