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