1use fsqlite_error::{FrankenError, Result};
4use fsqlite_types::PageSize;
5use serde::Serialize;
6use xxhash_rust::xxh3::xxh3_128;
7
8pub const SQLITE_DB_HEADER_SIZE: usize = 100;
10const SQLITE_DB_HEADER_SIZE_U16: u16 = 100;
11pub const SQLITE_DB_HEADER_RESERVED_OFFSET: usize = 20;
13pub const PAGE_CHECKSUM_RESERVED_BYTES: usize = 16;
15pub const WAL_HEADER_SIZE: usize = 32;
17pub const WAL_FRAME_HEADER_SIZE: usize = 24;
19
20const WAL_HEADER_SALT1_OFFSET: usize = 16;
21const WAL_HEADER_SALT2_OFFSET: usize = 20;
22const WAL_HEADER_CKSUM1_OFFSET: usize = 24;
23const WAL_HEADER_CKSUM2_OFFSET: usize = 28;
24
25const WAL_FRAME_DB_SIZE_OFFSET: usize = 4;
26const WAL_FRAME_SALT1_OFFSET: usize = 8;
27const WAL_FRAME_SALT2_OFFSET: usize = 12;
28const WAL_FRAME_CKSUM1_OFFSET: usize = 16;
29const WAL_FRAME_CKSUM2_OFFSET: usize = 20;
30const SQLITE_DB_HEADER_MAGIC: [u8; 16] = *b"SQLite format 3\0";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum HashTier {
35 Integrity,
36 ContentAddressing,
37 Protocol,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub struct SqliteWalChecksum {
43 pub s1: u32,
44 pub s2: u32,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub struct WalChecksumTransform {
55 pub a11: u32,
56 pub a12: u32,
57 pub a21: u32,
58 pub a22: u32,
59 pub c1: u32,
60 pub c2: u32,
61}
62
63impl WalChecksumTransform {
64 #[must_use]
66 pub const fn identity() -> Self {
67 Self {
68 a11: 1,
69 a12: 0,
70 a21: 0,
71 a22: 1,
72 c1: 0,
73 c2: 0,
74 }
75 }
76
77 #[must_use]
79 pub fn apply(self, seed: SqliteWalChecksum) -> SqliteWalChecksum {
80 SqliteWalChecksum {
81 s1: self
82 .a11
83 .wrapping_mul(seed.s1)
84 .wrapping_add(self.a12.wrapping_mul(seed.s2))
85 .wrapping_add(self.c1),
86 s2: self
87 .a21
88 .wrapping_mul(seed.s1)
89 .wrapping_add(self.a22.wrapping_mul(seed.s2))
90 .wrapping_add(self.c2),
91 }
92 }
93
94 #[must_use]
96 pub fn then(self, next: Self) -> Self {
97 Self {
98 a11: next
99 .a11
100 .wrapping_mul(self.a11)
101 .wrapping_add(next.a12.wrapping_mul(self.a21)),
102 a12: next
103 .a11
104 .wrapping_mul(self.a12)
105 .wrapping_add(next.a12.wrapping_mul(self.a22)),
106 a21: next
107 .a21
108 .wrapping_mul(self.a11)
109 .wrapping_add(next.a22.wrapping_mul(self.a21)),
110 a22: next
111 .a21
112 .wrapping_mul(self.a12)
113 .wrapping_add(next.a22.wrapping_mul(self.a22)),
114 c1: next
115 .a11
116 .wrapping_mul(self.c1)
117 .wrapping_add(next.a12.wrapping_mul(self.c2))
118 .wrapping_add(next.c1),
119 c2: next
120 .a21
121 .wrapping_mul(self.c1)
122 .wrapping_add(next.a22.wrapping_mul(self.c2))
123 .wrapping_add(next.c2),
124 }
125 }
126
127 #[must_use]
128 fn linear_coefficients_for_chunk_count(chunk_count: usize) -> (u32, u32, u32, u32) {
129 fn multiply(
130 left: (u32, u32, u32, u32),
131 right: (u32, u32, u32, u32),
132 ) -> (u32, u32, u32, u32) {
133 let (l11, l12, l21, l22) = left;
134 let (r11, r12, r21, r22) = right;
135 (
136 l11.wrapping_mul(r11).wrapping_add(l12.wrapping_mul(r21)),
137 l11.wrapping_mul(r12).wrapping_add(l12.wrapping_mul(r22)),
138 l21.wrapping_mul(r11).wrapping_add(l22.wrapping_mul(r21)),
139 l21.wrapping_mul(r12).wrapping_add(l22.wrapping_mul(r22)),
140 )
141 }
142
143 let mut result = (1, 0, 0, 1);
144 let mut base = (1, 1, 1, 2);
145 let mut exp = chunk_count;
146 while exp != 0 {
147 if exp & 1 == 1 {
148 result = multiply(result, base);
149 }
150 exp >>= 1;
151 if exp != 0 {
152 base = multiply(base, base);
153 }
154 }
155 result
156 }
157
158 pub fn from_aligned_bytes(data: &[u8], big_endian_checksum_words: bool) -> Result<Self> {
160 if !data.len().is_multiple_of(8) {
161 return Err(FrankenError::WalCorrupt {
162 detail: format!(
163 "WAL checksum transform input must be 8-byte aligned, got {} bytes",
164 data.len()
165 ),
166 });
167 }
168
169 let (a11, a12, a21, a22) = Self::linear_coefficients_for_chunk_count(data.len() / 8);
170 let mut c1 = 0_u32;
171 let mut c2 = 0_u32;
172 let (chunks, _rest) = data.as_chunks::<8>();
173 for chunk in chunks {
174 let x0 = decode_u32_words(&chunk[..4], big_endian_checksum_words);
175 let x1 = decode_u32_words(&chunk[4..], big_endian_checksum_words);
176 c1 = c1.wrapping_add(x0).wrapping_add(c2);
177 c2 = c2.wrapping_add(x1).wrapping_add(c1);
178 }
179
180 Ok(Self {
181 a11,
182 a12,
183 a21,
184 a22,
185 c1,
186 c2,
187 })
188 }
189
190 pub fn for_wal_frame(
192 frame: &[u8],
193 page_size: usize,
194 big_endian_checksum_words: bool,
195 ) -> Result<Self> {
196 ensure_frame_len(frame, page_size)?;
197 let header = Self::from_aligned_bytes(&frame[..8], big_endian_checksum_words)?;
198 let payload = Self::from_aligned_bytes(
199 &frame[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + page_size],
200 big_endian_checksum_words,
201 )?;
202 Ok(header.then(payload))
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
208pub struct WalSalts {
209 pub salt1: u32,
210 pub salt2: u32,
211}
212
213impl WalSalts {
214 #[cfg(not(target_arch = "wasm32"))]
223 #[must_use]
224 pub fn generate() -> Self {
225 Self {
226 salt1: rand::random::<u32>(),
227 salt2: rand::random::<u32>(),
228 }
229 }
230
231 #[cfg(target_arch = "wasm32")]
234 #[must_use]
235 pub fn generate() -> Self {
236 use core::sync::atomic::{AtomicU32, Ordering};
237 static NEXT: AtomicU32 = AtomicU32::new(1);
238 let n = NEXT.fetch_add(1, Ordering::Relaxed);
239 Self {
240 salt1: n,
241 salt2: n.wrapping_mul(0x9E37_79B9),
242 }
243 }
244
245 #[must_use]
251 pub fn next_generation(self) -> Self {
252 Self {
253 salt1: self.salt1.wrapping_add(1),
254 salt2: Self::generate().salt2,
255 }
256 }
257}
258
259pub const WAL_MAGIC_LE: u32 = 0x377F_0682;
261
262pub const WAL_MAGIC_BE: u32 = 0x377F_0683;
264
265pub const WAL_FORMAT_VERSION: u32 = 3_007_000;
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub struct WalHeader {
284 pub magic: u32,
286 pub format_version: u32,
288 pub page_size: u32,
290 pub checkpoint_seq: u32,
292 pub salts: WalSalts,
294 pub checksum: SqliteWalChecksum,
296}
297
298impl WalHeader {
299 #[must_use]
301 pub const fn big_endian_checksum(&self) -> bool {
302 self.magic == WAL_MAGIC_BE
303 }
304
305 pub fn from_bytes(buf: &[u8]) -> Result<Self> {
307 if buf.len() < WAL_HEADER_SIZE {
308 return Err(FrankenError::WalCorrupt {
309 detail: format!(
310 "WAL header too small: expected >= {WAL_HEADER_SIZE}, got {}",
311 buf.len()
312 ),
313 });
314 }
315 let magic = read_be_u32_at(buf, 0);
316 if magic != WAL_MAGIC_LE && magic != WAL_MAGIC_BE {
317 return Err(FrankenError::WalCorrupt {
318 detail: format!("invalid WAL magic: {magic:#010x}"),
319 });
320 }
321 let format_version = read_be_u32_at(buf, 4);
322 if format_version != WAL_FORMAT_VERSION {
323 return Err(FrankenError::WalCorrupt {
324 detail: format!(
325 "unsupported WAL format version: {format_version} (expected {WAL_FORMAT_VERSION})"
326 ),
327 });
328 }
329 let page_size = read_be_u32_at(buf, 8);
330 ensure_valid_wal_header_page_size(page_size)?;
331
332 Ok(Self {
333 magic,
334 format_version,
335 page_size,
336 checkpoint_seq: read_be_u32_at(buf, 12),
337 salts: WalSalts {
338 salt1: read_be_u32_at(buf, WAL_HEADER_SALT1_OFFSET),
339 salt2: read_be_u32_at(buf, WAL_HEADER_SALT2_OFFSET),
340 },
341 checksum: SqliteWalChecksum {
342 s1: read_be_u32_at(buf, WAL_HEADER_CKSUM1_OFFSET),
343 s2: read_be_u32_at(buf, WAL_HEADER_CKSUM2_OFFSET),
344 },
345 })
346 }
347
348 pub fn to_bytes(&self) -> Result<[u8; WAL_HEADER_SIZE]> {
350 ensure_valid_wal_header_page_size(self.page_size)?;
351
352 let mut buf = [0u8; WAL_HEADER_SIZE];
353 write_be_u32_at(&mut buf, 0, self.magic);
354 write_be_u32_at(&mut buf, 4, self.format_version);
355 write_be_u32_at(&mut buf, 8, self.page_size);
356 write_be_u32_at(&mut buf, 12, self.checkpoint_seq);
357 write_be_u32_at(&mut buf, WAL_HEADER_SALT1_OFFSET, self.salts.salt1);
358 write_be_u32_at(&mut buf, WAL_HEADER_SALT2_OFFSET, self.salts.salt2);
359 let checksum = sqlite_wal_checksum(
361 &buf[..WAL_HEADER_CKSUM1_OFFSET],
362 0,
363 0,
364 self.big_endian_checksum(),
365 )?;
366 write_be_u32_at(&mut buf, WAL_HEADER_CKSUM1_OFFSET, checksum.s1);
367 write_be_u32_at(&mut buf, WAL_HEADER_CKSUM2_OFFSET, checksum.s2);
368 Ok(buf)
369 }
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub struct WalFrameHeader {
386 pub page_number: u32,
388 pub db_size: u32,
390 pub salts: WalSalts,
392 pub checksum: SqliteWalChecksum,
394}
395
396impl WalFrameHeader {
397 #[must_use]
399 pub const fn is_commit(&self) -> bool {
400 self.db_size > 0
401 }
402
403 pub fn from_bytes(buf: &[u8]) -> Result<Self> {
405 if buf.len() < WAL_FRAME_HEADER_SIZE {
406 return Err(FrankenError::WalCorrupt {
407 detail: format!(
408 "WAL frame header too small: expected >= {WAL_FRAME_HEADER_SIZE}, got {}",
409 buf.len()
410 ),
411 });
412 }
413 Ok(Self {
414 page_number: read_be_u32_at(buf, 0),
415 db_size: read_be_u32_at(buf, WAL_FRAME_DB_SIZE_OFFSET),
416 salts: WalSalts {
417 salt1: read_be_u32_at(buf, WAL_FRAME_SALT1_OFFSET),
418 salt2: read_be_u32_at(buf, WAL_FRAME_SALT2_OFFSET),
419 },
420 checksum: SqliteWalChecksum {
421 s1: read_be_u32_at(buf, WAL_FRAME_CKSUM1_OFFSET),
422 s2: read_be_u32_at(buf, WAL_FRAME_CKSUM2_OFFSET),
423 },
424 })
425 }
426
427 pub fn to_bytes(&self) -> [u8; WAL_FRAME_HEADER_SIZE] {
432 let mut buf = [0u8; WAL_FRAME_HEADER_SIZE];
433 write_be_u32_at(&mut buf, 0, self.page_number);
434 write_be_u32_at(&mut buf, WAL_FRAME_DB_SIZE_OFFSET, self.db_size);
435 write_be_u32_at(&mut buf, WAL_FRAME_SALT1_OFFSET, self.salts.salt1);
436 write_be_u32_at(&mut buf, WAL_FRAME_SALT2_OFFSET, self.salts.salt2);
437 write_be_u32_at(&mut buf, WAL_FRAME_CKSUM1_OFFSET, self.checksum.s1);
438 write_be_u32_at(&mut buf, WAL_FRAME_CKSUM2_OFFSET, self.checksum.s2);
439 buf
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
445pub enum WalChainInvalidReason {
446 HeaderChecksumMismatch,
447 TruncatedFrame,
448 SaltMismatch,
449 FrameSaltMismatch,
450 FrameChecksumMismatch,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub struct WalChainValidation {
456 pub valid: bool,
457 pub valid_frames: usize,
458 pub replayable_frames: usize,
459 pub first_invalid_frame: Option<usize>,
460 pub reason: Option<WalChainInvalidReason>,
461 pub last_commit_frame: Option<usize>,
462
463 pub header_valid: bool,
465 pub valid_frame_count: usize,
466 pub replayable_frame_count: usize,
467 pub first_invalid_reason: Option<WalChainInvalidReason>,
468 pub replayable_prefix_len: usize,
469}
470
471impl WalChainValidation {
472 fn from_core(
473 valid: bool,
474 valid_frames: usize,
475 replayable_frames: usize,
476 first_invalid_frame: Option<usize>,
477 reason: Option<WalChainInvalidReason>,
478 last_commit_frame: Option<usize>,
479 frame_size: usize,
480 ) -> Self {
481 Self {
482 valid,
483 valid_frames,
484 replayable_frames,
485 first_invalid_frame,
486 reason,
487 last_commit_frame,
488 header_valid: valid || reason != Some(WalChainInvalidReason::HeaderChecksumMismatch),
489 valid_frame_count: valid_frames,
490 replayable_frame_count: replayable_frames,
491 first_invalid_reason: reason,
492 replayable_prefix_len: WAL_HEADER_SIZE + replayable_frames * frame_size,
493 }
494 }
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum IntegrityCheckLevel {
500 Page,
501 BtreeStructural,
502 RecordFormat,
503 CrossReference,
504 Schema,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq)]
509pub struct IntegrityCheckIssue {
510 pub level: IntegrityCheckLevel,
511 pub page_number: Option<u32>,
512 pub detail: String,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
517pub struct IntegrityCheckReport {
518 pub pages_checked: usize,
519 pub issues: Vec<IntegrityCheckIssue>,
520}
521
522impl IntegrityCheckReport {
523 #[must_use]
525 pub fn ok(pages_checked: usize) -> Self {
526 Self {
527 pages_checked,
528 issues: Vec::new(),
529 }
530 }
531
532 #[must_use]
534 pub fn is_ok(&self) -> bool {
535 self.issues.is_empty()
536 }
537
538 #[must_use]
540 pub fn sqlite_messages(&self) -> Vec<String> {
541 if self.is_ok() {
542 vec!["ok".to_owned()]
543 } else {
544 self.issues
545 .iter()
546 .map(|issue| issue.detail.clone())
547 .collect()
548 }
549 }
550
551 fn push(
552 &mut self,
553 level: IntegrityCheckLevel,
554 page_number: Option<u32>,
555 detail: impl Into<String>,
556 ) {
557 self.issues.push(IntegrityCheckIssue {
558 level,
559 page_number,
560 detail: detail.into(),
561 });
562 }
563}
564
565pub const BTREE_PAGE_TYPE_FLAGS: [u8; 4] = [0x02, 0x05, 0x0A, 0x0D];
567
568pub const CRASH_MODEL_SECTOR_SIZES: [usize; 3] = [512, 1024, 4096];
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
573pub enum ChecksumFailureKind {
574 WalFrameChecksumMismatch,
575 Xxh3PageChecksumMismatch,
576 Crc32cSymbolMismatch,
577 DbFileCorruption,
578}
579
580#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
582pub enum RecoveryAction {
583 AttemptWalFecRepair,
584 TruncateWalAtFirstInvalidFrame,
585 EvictCacheAndRetryFromWal,
586 ExcludeCorruptedSymbolAndContinue,
587 ReportPersistentCorruption,
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
592pub enum WalFecRepairOutcome {
593 Repaired,
594 InsufficientSymbols,
595 SourceHashMismatch,
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600pub enum WalRecoveryDecision {
601 Repaired,
602 Truncated,
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq)]
607pub struct CrashModelContract {
608 flags: u8,
609}
610
611impl CrashModelContract {
612 pub const CRASH_AT_ANY_POINT: u8 = 1 << 0;
613 pub const FSYNC_IS_DURABILITY_BARRIER: u8 = 1 << 1;
614 pub const WRITES_REORDER_WITHOUT_FSYNC: u8 = 1 << 2;
615 pub const BITROT_EXISTS: u8 = 1 << 3;
616 pub const METADATA_MAY_REQUIRE_DIRECTORY_FSYNC: u8 = 1 << 4;
617
618 #[must_use]
619 pub fn crash_at_any_point(self) -> bool {
620 self.flags & Self::CRASH_AT_ANY_POINT != 0
621 }
622
623 #[must_use]
624 pub fn fsync_is_durability_barrier(self) -> bool {
625 self.flags & Self::FSYNC_IS_DURABILITY_BARRIER != 0
626 }
627
628 #[must_use]
629 pub fn writes_reorder_without_fsync(self) -> bool {
630 self.flags & Self::WRITES_REORDER_WITHOUT_FSYNC != 0
631 }
632
633 #[must_use]
634 pub fn bitrot_exists(self) -> bool {
635 self.flags & Self::BITROT_EXISTS != 0
636 }
637
638 #[must_use]
639 pub fn metadata_may_require_directory_fsync(self) -> bool {
640 self.flags & Self::METADATA_MAY_REQUIRE_DIRECTORY_FSYNC != 0
641 }
642}
643
644impl Default for CrashModelContract {
645 fn default() -> Self {
646 Self {
647 flags: Self::CRASH_AT_ANY_POINT
648 | Self::FSYNC_IS_DURABILITY_BARRIER
649 | Self::WRITES_REORDER_WITHOUT_FSYNC
650 | Self::BITROT_EXISTS
651 | Self::METADATA_MAY_REQUIRE_DIRECTORY_FSYNC,
652 }
653 }
654}
655
656#[must_use]
658pub fn crash_model_contract() -> CrashModelContract {
659 CrashModelContract::default()
660}
661
662#[must_use]
664pub fn supports_torn_write_sector_size(bytes_per_sector: usize) -> bool {
665 CRASH_MODEL_SECTOR_SIZES.contains(&bytes_per_sector)
666}
667
668#[must_use]
670pub fn is_valid_btree_page_type(page_type: u8) -> bool {
671 BTREE_PAGE_TYPE_FLAGS.contains(&page_type)
672}
673
674pub fn integrity_check_level1_page(
676 page: &[u8],
677 page_number: u32,
678 is_btree_page: bool,
679 verify_xxh3_trailer: bool,
680) -> Result<IntegrityCheckReport> {
681 let mut report = IntegrityCheckReport::ok(1);
682
683 if page.is_empty() {
684 report.push(
685 IntegrityCheckLevel::Page,
686 Some(page_number),
687 format!("page {page_number}: empty page buffer"),
688 );
689 return Ok(report);
690 }
691
692 if is_btree_page {
693 let page_type = page[0];
694 if !is_valid_btree_page_type(page_type) {
695 report.push(
696 IntegrityCheckLevel::Page,
697 Some(page_number),
698 format!("page {page_number}: invalid b-tree page type 0x{page_type:02x}"),
699 );
700 return Ok(report);
701 }
702
703 let header_size = if page_type == 0x02 || page_type == 0x05 {
704 12
705 } else {
706 8
707 };
708
709 if page.len() < header_size {
710 report.push(
711 IntegrityCheckLevel::Page,
712 Some(page_number),
713 format!(
714 "page {page_number}: b-tree header too small (need {header_size}, got {})",
715 page.len()
716 ),
717 );
718 return Ok(report);
719 }
720
721 let first_freeblock = u16::from_be_bytes([page[1], page[2]]);
722 if first_freeblock != 0 && usize::from(first_freeblock) >= page.len() {
723 report.push(
724 IntegrityCheckLevel::Page,
725 Some(page_number),
726 format!(
727 "page {page_number}: first freeblock offset out of range ({first_freeblock})"
728 ),
729 );
730 }
731
732 let cell_count = u16::from_be_bytes([page[3], page[4]]);
733 let raw_cell_content_offset = u16::from_be_bytes([page[5], page[6]]);
734 let cell_content_offset = if raw_cell_content_offset == 0
735 && (page.len() == 65_536 || (page_number == 1 && page.len() == 65_536 - 100))
736 {
737 page.len()
738 } else {
739 usize::from(raw_cell_content_offset)
740 };
741
742 if cell_content_offset == 0 || cell_content_offset > page.len() {
743 report.push(
744 IntegrityCheckLevel::Page,
745 Some(page_number),
746 format!(
747 "page {page_number}: cell content offset out of range ({cell_content_offset})"
748 ),
749 );
750 }
751
752 let pointer_bytes = usize::from(cell_count) * 2;
753 if header_size + pointer_bytes > page.len() {
754 report.push(
755 IntegrityCheckLevel::Page,
756 Some(page_number),
757 format!(
758 "page {page_number}: cell pointer array exceeds page bounds (cells={cell_count})"
759 ),
760 );
761 } else if header_size + pointer_bytes > cell_content_offset {
762 report.push(
763 IntegrityCheckLevel::Page,
764 Some(page_number),
765 format!(
766 "page {page_number}: cell pointer array overlaps cell content area (cells={cell_count}, offset={cell_content_offset})"
767 ),
768 );
769 }
770
771 let fragmented = page[7];
772 if fragmented > 60 {
773 report.push(
774 IntegrityCheckLevel::Page,
775 Some(page_number),
776 format!("page {page_number}: fragmented free bytes out of range ({fragmented})"),
777 );
778 }
779 }
780
781 if verify_xxh3_trailer {
782 match verify_page_checksum(page) {
783 Ok(true) => {}
784 Ok(false) => {
785 report.push(
786 IntegrityCheckLevel::Page,
787 Some(page_number),
788 format!("page {page_number}: xxh3 page checksum mismatch"),
789 );
790 }
791 Err(err) => {
792 report.push(
793 IntegrityCheckLevel::Page,
794 Some(page_number),
795 format!("page {page_number}: xxh3 verification error: {err}"),
796 );
797 }
798 }
799 }
800
801 Ok(report)
802}
803
804#[must_use]
806pub fn integrity_check_database_header(db_bytes: &[u8]) -> IntegrityCheckReport {
807 let mut report = IntegrityCheckReport::ok(1);
808 if db_bytes.len() < SQLITE_DB_HEADER_SIZE {
809 report.push(
810 IntegrityCheckLevel::Page,
811 Some(1),
812 format!(
813 "database header too small: expected >= {SQLITE_DB_HEADER_SIZE}, got {}",
814 db_bytes.len()
815 ),
816 );
817 return report;
818 }
819
820 if db_bytes[..SQLITE_DB_HEADER_MAGIC.len()] != SQLITE_DB_HEADER_MAGIC {
821 report.push(
822 IntegrityCheckLevel::Page,
823 Some(1),
824 "database header magic mismatch".to_owned(),
825 );
826 }
827
828 let page_size_raw = u16::from_be_bytes([db_bytes[16], db_bytes[17]]);
829 let page_size = if page_size_raw == 1 {
830 65_536
831 } else {
832 usize::from(page_size_raw)
833 };
834 if !(512..=65_536).contains(&page_size) || !page_size.is_power_of_two() {
835 report.push(
836 IntegrityCheckLevel::Page,
837 Some(1),
838 format!("database header page size out of range ({page_size})"),
839 );
840 }
841
842 report
843}
844
845pub fn integrity_check_sqlite_file_level1(db_bytes: &[u8]) -> Result<IntegrityCheckReport> {
847 let header_report = integrity_check_database_header(db_bytes);
848
849 let page_report = if db_bytes.len() >= SQLITE_DB_HEADER_SIZE + 8 {
850 let page_size = match sqlite_page_size_from_header(db_bytes) {
851 Some(ps) => ps,
852 None => {
853 let mut report = IntegrityCheckReport::ok(1);
858 report.push(
859 IntegrityCheckLevel::Page,
860 None,
861 "cannot determine page size from database header; \
862 header may be corrupted — skipping page checks"
863 .to_owned(),
864 );
865 for issue in header_report.issues {
866 report.issues.push(issue);
867 }
868 return Ok(report);
869 }
870 };
871 let first_page_end = page_size.min(db_bytes.len());
872 if first_page_end > SQLITE_DB_HEADER_SIZE {
873 let mut first_page = db_bytes[SQLITE_DB_HEADER_SIZE..first_page_end].to_vec();
874 normalize_first_page_header_offsets(&mut first_page);
875 integrity_check_level1_page(&first_page, 1, true, false)?
876 } else {
877 let mut report = IntegrityCheckReport::ok(1);
878 report.push(
879 IntegrityCheckLevel::Page,
880 Some(1),
881 "database first page payload missing".to_owned(),
882 );
883 report
884 }
885 } else {
886 let mut report = IntegrityCheckReport::ok(1);
887 report.push(
888 IntegrityCheckLevel::Page,
889 Some(1),
890 "database missing first b-tree page header bytes".to_owned(),
891 );
892 report
893 };
894
895 Ok(merge_integrity_reports(&[header_report, page_report]))
896}
897
898#[must_use]
900pub fn integrity_check_level2_btree(
901 page_number: u32,
902 page_size: usize,
903 cell_spans: &[(u16, u32)],
904 keys: &[i64],
905) -> IntegrityCheckReport {
906 let mut report = IntegrityCheckReport::ok(1);
907
908 if page_size == 0 {
909 report.push(
910 IntegrityCheckLevel::BtreeStructural,
911 Some(page_number),
912 format!("page {page_number}: invalid page size 0 for structural check"),
913 );
914 return report;
915 }
916
917 let mut sorted_spans = cell_spans.to_vec();
918 sorted_spans.sort_unstable_by_key(|&(start, _)| start);
919
920 for (start, end) in &sorted_spans {
921 let start_usize = *start as usize;
922 let end_usize = *end as usize;
923 if start_usize >= end_usize || end_usize > page_size {
924 report.push(
925 IntegrityCheckLevel::BtreeStructural,
926 Some(page_number),
927 format!("page {page_number}: cell span out of bounds ({start}..{end})"),
928 );
929 }
930 }
931
932 for window in sorted_spans.windows(2) {
933 let (_, prev_end) = window[0];
934 let (next_start, _) = window[1];
935 if prev_end > u32::from(next_start) {
936 report.push(
937 IntegrityCheckLevel::BtreeStructural,
938 Some(page_number),
939 format!(
940 "page {page_number}: overlapping cell spans ({}) and ({})",
941 format_args!("{}..{}", window[0].0, window[0].1),
942 format_args!("{}..{}", window[1].0, window[1].1)
943 ),
944 );
945 break;
946 }
947 }
948
949 if keys.windows(2).any(|window| window[0] > window[1]) {
950 report.push(
951 IntegrityCheckLevel::BtreeStructural,
952 Some(page_number),
953 format!("page {page_number}: keys out of order"),
954 );
955 }
956
957 report
958}
959
960#[must_use]
962pub fn integrity_check_level3_overflow_chain(
963 page_number: u32,
964 overflow_chain: &[u32],
965 max_page_number: u32,
966) -> IntegrityCheckReport {
967 let mut report = IntegrityCheckReport::ok(1);
968 let mut seen = std::collections::HashSet::new();
969
970 for overflow_page in overflow_chain {
971 if *overflow_page == 0 || *overflow_page > max_page_number {
972 report.push(
973 IntegrityCheckLevel::RecordFormat,
974 Some(page_number),
975 format!(
976 "page {page_number}: broken overflow chain references page {overflow_page}"
977 ),
978 );
979 break;
980 }
981 if !seen.insert(*overflow_page) {
982 report.push(
983 IntegrityCheckLevel::RecordFormat,
984 Some(page_number),
985 format!("page {page_number}: broken overflow chain cycle at page {overflow_page}"),
986 );
987 break;
988 }
989 }
990
991 report
992}
993
994#[must_use]
996pub fn integrity_check_level4_cross_reference(
997 expected_total_pages: u32,
998 accounted_pages: &[u32],
999) -> IntegrityCheckReport {
1000 let pages_checked = usize::try_from(expected_total_pages).unwrap_or(usize::MAX);
1001 let mut report = IntegrityCheckReport::ok(pages_checked);
1002 let mut seen = std::collections::HashSet::new();
1003
1004 for page in accounted_pages {
1005 if *page == 0 || *page > expected_total_pages {
1006 report.push(
1007 IntegrityCheckLevel::CrossReference,
1008 Some(*page),
1009 format!("page {page}: cross-reference contains out-of-range page reference"),
1010 );
1011 continue;
1012 }
1013 if !seen.insert(*page) {
1014 report.push(
1015 IntegrityCheckLevel::CrossReference,
1016 Some(*page),
1017 format!("page {page}: appears in multiple b-tree ownership sets"),
1018 );
1019 }
1020 }
1021
1022 for expected_page in 1..=expected_total_pages {
1023 if !seen.contains(&expected_page) {
1024 report.push(
1025 IntegrityCheckLevel::CrossReference,
1026 Some(expected_page),
1027 format!(
1028 "page {expected_page}: not accounted for by any b-tree/freelist/pointer-map"
1029 ),
1030 );
1031 }
1032 }
1033
1034 report
1035}
1036
1037#[must_use]
1039pub fn integrity_check_level5_schema(schema_entries: &[String]) -> IntegrityCheckReport {
1040 let mut report = IntegrityCheckReport::ok(schema_entries.len());
1041
1042 if schema_entries.is_empty() {
1043 report.push(
1044 IntegrityCheckLevel::Schema,
1045 None,
1046 "malformed sqlite_master: no entries".to_owned(),
1047 );
1048 return report;
1049 }
1050
1051 for (index, entry) in schema_entries.iter().enumerate() {
1052 if !is_valid_schema_sql(entry) {
1053 report.push(
1054 IntegrityCheckLevel::Schema,
1055 None,
1056 format!("sqlite_master row {index}: malformed SQL entry"),
1057 );
1058 }
1059 }
1060
1061 report
1062}
1063
1064#[must_use]
1066pub fn merge_integrity_reports(reports: &[IntegrityCheckReport]) -> IntegrityCheckReport {
1067 let pages_checked = reports.iter().map(|report| report.pages_checked).sum();
1068 let mut merged = IntegrityCheckReport::ok(pages_checked);
1069 for report in reports {
1070 merged.issues.extend(report.issues.clone());
1071 }
1072 merged
1073}
1074
1075#[must_use]
1077pub fn recovery_action_for_checksum_failure(
1078 failure: ChecksumFailureKind,
1079 surviving_symbols: Option<usize>,
1080 required_symbols: Option<usize>,
1081) -> RecoveryAction {
1082 match failure {
1083 ChecksumFailureKind::WalFrameChecksumMismatch => {
1084 if let (Some(surviving), Some(required)) = (surviving_symbols, required_symbols) {
1085 if surviving >= required {
1086 RecoveryAction::AttemptWalFecRepair
1087 } else {
1088 RecoveryAction::TruncateWalAtFirstInvalidFrame
1089 }
1090 } else {
1091 RecoveryAction::TruncateWalAtFirstInvalidFrame
1092 }
1093 }
1094 ChecksumFailureKind::Xxh3PageChecksumMismatch => RecoveryAction::EvictCacheAndRetryFromWal,
1095 ChecksumFailureKind::Crc32cSymbolMismatch => {
1096 RecoveryAction::ExcludeCorruptedSymbolAndContinue
1097 }
1098 ChecksumFailureKind::DbFileCorruption => RecoveryAction::ReportPersistentCorruption,
1099 }
1100}
1101
1102#[must_use]
1104pub fn attempt_wal_fec_repair(
1105 reconstructed_payload: &[u8],
1106 expected_source_hash: Xxh3Checksum128,
1107 surviving_symbols: usize,
1108 required_symbols: usize,
1109) -> WalFecRepairOutcome {
1110 if surviving_symbols < required_symbols {
1111 return WalFecRepairOutcome::InsufficientSymbols;
1112 }
1113 if verify_wal_fec_source_hash(reconstructed_payload, expected_source_hash) {
1114 WalFecRepairOutcome::Repaired
1115 } else {
1116 WalFecRepairOutcome::SourceHashMismatch
1117 }
1118}
1119
1120#[must_use]
1122pub fn recover_wal_frame_checksum_mismatch(
1123 reconstructed_payload: Option<&[u8]>,
1124 expected_source_hash: Option<Xxh3Checksum128>,
1125 surviving_symbols: usize,
1126 required_symbols: usize,
1127) -> WalRecoveryDecision {
1128 let action = recovery_action_for_checksum_failure(
1129 ChecksumFailureKind::WalFrameChecksumMismatch,
1130 Some(surviving_symbols),
1131 Some(required_symbols),
1132 );
1133
1134 if action != RecoveryAction::AttemptWalFecRepair {
1135 return WalRecoveryDecision::Truncated;
1136 }
1137
1138 let (Some(payload), Some(expected_hash)) = (reconstructed_payload, expected_source_hash) else {
1139 return WalRecoveryDecision::Truncated;
1140 };
1141
1142 match attempt_wal_fec_repair(payload, expected_hash, surviving_symbols, required_symbols) {
1143 WalFecRepairOutcome::Repaired => WalRecoveryDecision::Repaired,
1144 WalFecRepairOutcome::InsufficientSymbols | WalFecRepairOutcome::SourceHashMismatch => {
1145 WalRecoveryDecision::Truncated
1146 }
1147 }
1148}
1149
1150pub fn detect_torn_write_in_wal(
1152 wal_bytes: &[u8],
1153 page_size: usize,
1154 big_endian_checksum_words: bool,
1155) -> Result<bool> {
1156 let validation = validate_wal_chain(wal_bytes, page_size, big_endian_checksum_words)?;
1157 Ok(matches!(
1158 validation.reason,
1159 Some(WalChainInvalidReason::TruncatedFrame | WalChainInvalidReason::FrameChecksumMismatch)
1160 ))
1161}
1162
1163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1165pub struct Xxh3Checksum128 {
1166 pub low: u64,
1167 pub high: u64,
1168}
1169
1170impl Xxh3Checksum128 {
1171 #[must_use]
1173 pub fn compute(data: &[u8]) -> Self {
1174 from_u128_le(xxh3_128(data))
1175 }
1176
1177 #[must_use]
1179 pub fn verify(&self, data: &[u8]) -> bool {
1180 *self == Self::compute(data)
1181 }
1182
1183 #[must_use]
1185 pub fn to_le_bytes(self) -> [u8; 16] {
1186 let mut out = [0_u8; 16];
1187 out[..8].copy_from_slice(&self.low.to_le_bytes());
1188 out[8..].copy_from_slice(&self.high.to_le_bytes());
1189 out
1190 }
1191}
1192
1193pub fn configure_page_checksum_reserved_bytes(db_header: &mut [u8], enabled: bool) -> Result<()> {
1195 ensure_min_len(
1196 db_header,
1197 SQLITE_DB_HEADER_RESERVED_OFFSET + 1,
1198 "database header",
1199 )?;
1200 db_header[SQLITE_DB_HEADER_RESERVED_OFFSET] = if enabled {
1201 u8::try_from(PAGE_CHECKSUM_RESERVED_BYTES).expect("reserved-byte constant fits in u8")
1202 } else {
1203 0
1204 };
1205 Ok(())
1206}
1207
1208pub fn page_checksum_reserved_bytes(db_header: &[u8]) -> Result<u8> {
1210 ensure_min_len(
1211 db_header,
1212 SQLITE_DB_HEADER_RESERVED_OFFSET + 1,
1213 "database header",
1214 )?;
1215 Ok(db_header[SQLITE_DB_HEADER_RESERVED_OFFSET])
1216}
1217
1218pub fn zero_page_checksum_trailer(page: &mut [u8]) -> Result<()> {
1220 if page.len() < PAGE_CHECKSUM_RESERVED_BYTES {
1221 return Err(FrankenError::WalCorrupt {
1222 detail: format!(
1223 "page too small for checksum trailer: expected >= {PAGE_CHECKSUM_RESERVED_BYTES}, got {}",
1224 page.len()
1225 ),
1226 });
1227 }
1228
1229 let start = page.len() - PAGE_CHECKSUM_RESERVED_BYTES;
1230 page[start..].fill(0);
1231 Ok(())
1232}
1233
1234pub fn write_page_checksum(page: &mut [u8]) -> Result<Xxh3Checksum128> {
1236 if page.len() < PAGE_CHECKSUM_RESERVED_BYTES {
1237 return Err(FrankenError::WalCorrupt {
1238 detail: format!(
1239 "page too small for checksum trailer: expected >= {PAGE_CHECKSUM_RESERVED_BYTES}, got {}",
1240 page.len()
1241 ),
1242 });
1243 }
1244
1245 let payload_end = page.len() - PAGE_CHECKSUM_RESERVED_BYTES;
1246 let digest = Xxh3Checksum128::compute(&page[..payload_end]);
1247 page[payload_end..].copy_from_slice(&digest.to_le_bytes());
1248 Ok(digest)
1249}
1250
1251pub fn read_page_checksum(page: &[u8]) -> Result<Xxh3Checksum128> {
1253 if page.len() < PAGE_CHECKSUM_RESERVED_BYTES {
1254 return Err(FrankenError::WalCorrupt {
1255 detail: format!(
1256 "page too small for checksum trailer: expected >= {PAGE_CHECKSUM_RESERVED_BYTES}, got {}",
1257 page.len()
1258 ),
1259 });
1260 }
1261
1262 let checksum_start = page.len() - PAGE_CHECKSUM_RESERVED_BYTES;
1263 Ok(read_xxh3_from_bytes(
1264 &page[checksum_start..checksum_start + PAGE_CHECKSUM_RESERVED_BYTES],
1265 ))
1266}
1267
1268pub fn verify_page_checksum(page: &[u8]) -> Result<bool> {
1270 if page.len() < PAGE_CHECKSUM_RESERVED_BYTES {
1271 return Err(FrankenError::WalCorrupt {
1272 detail: format!(
1273 "page too small for checksum trailer: expected >= {PAGE_CHECKSUM_RESERVED_BYTES}, got {}",
1274 page.len()
1275 ),
1276 });
1277 }
1278
1279 let payload_end = page.len() - PAGE_CHECKSUM_RESERVED_BYTES;
1280 let expected = Xxh3Checksum128::compute(&page[..payload_end]);
1281 let actual = read_page_checksum(page)?;
1282 Ok(actual == expected)
1283}
1284
1285#[must_use]
1287pub fn wal_fec_source_hash_xxh3_128(page_payload: &[u8]) -> Xxh3Checksum128 {
1288 Xxh3Checksum128::compute(page_payload)
1289}
1290
1291#[must_use]
1293pub fn verify_wal_fec_source_hash(page_payload: &[u8], expected: Xxh3Checksum128) -> bool {
1294 wal_fec_source_hash_xxh3_128(page_payload) == expected
1295}
1296
1297pub fn read_wal_header_salts(wal_header: &[u8]) -> Result<WalSalts> {
1299 ensure_min_len(wal_header, WAL_HEADER_SIZE, "WAL header")?;
1300 Ok(WalSalts {
1301 salt1: read_be_u32_at(wal_header, WAL_HEADER_SALT1_OFFSET),
1302 salt2: read_be_u32_at(wal_header, WAL_HEADER_SALT2_OFFSET),
1303 })
1304}
1305
1306pub fn write_wal_header_salts(wal_header: &mut [u8], salts: WalSalts) -> Result<()> {
1308 ensure_min_len(wal_header, WAL_HEADER_SIZE, "WAL header")?;
1309 write_be_u32_at(wal_header, WAL_HEADER_SALT1_OFFSET, salts.salt1);
1310 write_be_u32_at(wal_header, WAL_HEADER_SALT2_OFFSET, salts.salt2);
1311 Ok(())
1312}
1313
1314pub fn read_wal_header_checksum(wal_header: &[u8]) -> Result<SqliteWalChecksum> {
1316 ensure_min_len(wal_header, WAL_HEADER_SIZE, "WAL header")?;
1317 Ok(SqliteWalChecksum {
1318 s1: read_be_u32_at(wal_header, WAL_HEADER_CKSUM1_OFFSET),
1319 s2: read_be_u32_at(wal_header, WAL_HEADER_CKSUM2_OFFSET),
1320 })
1321}
1322
1323pub fn write_wal_header_checksum(
1325 wal_header: &mut [u8],
1326 big_endian_checksum_words: bool,
1327) -> Result<SqliteWalChecksum> {
1328 ensure_min_len(wal_header, WAL_HEADER_SIZE, "WAL header")?;
1329 let checksum = wal_header_checksum(wal_header, big_endian_checksum_words)?;
1330 write_be_u32_at(wal_header, WAL_HEADER_CKSUM1_OFFSET, checksum.s1);
1331 write_be_u32_at(wal_header, WAL_HEADER_CKSUM2_OFFSET, checksum.s2);
1332 Ok(checksum)
1333}
1334
1335pub fn wal_header_checksum(
1337 wal_header: &[u8],
1338 big_endian_checksum_words: bool,
1339) -> Result<SqliteWalChecksum> {
1340 ensure_min_len(wal_header, WAL_HEADER_SIZE, "WAL header")?;
1341 sqlite_wal_checksum(
1342 &wal_header[..WAL_HEADER_CKSUM1_OFFSET],
1343 0,
1344 0,
1345 big_endian_checksum_words,
1346 )
1347}
1348
1349pub fn validate_wal_header_checksum(
1351 wal_header: &[u8],
1352 big_endian_checksum_words: bool,
1353) -> Result<bool> {
1354 let expected = wal_header_checksum(wal_header, big_endian_checksum_words)?;
1355 let actual = read_wal_header_checksum(wal_header)?;
1356 Ok(actual == expected)
1357}
1358
1359pub fn read_wal_frame_salts(frame_header: &[u8]) -> Result<WalSalts> {
1361 ensure_min_len(frame_header, WAL_FRAME_HEADER_SIZE, "WAL frame header")?;
1362 Ok(WalSalts {
1363 salt1: read_be_u32_at(frame_header, WAL_FRAME_SALT1_OFFSET),
1364 salt2: read_be_u32_at(frame_header, WAL_FRAME_SALT2_OFFSET),
1365 })
1366}
1367
1368pub fn write_wal_frame_salts(frame_header: &mut [u8], salts: WalSalts) -> Result<()> {
1370 ensure_min_len(frame_header, WAL_FRAME_HEADER_SIZE, "WAL frame header")?;
1371 write_be_u32_at(frame_header, WAL_FRAME_SALT1_OFFSET, salts.salt1);
1372 write_be_u32_at(frame_header, WAL_FRAME_SALT2_OFFSET, salts.salt2);
1373 Ok(())
1374}
1375
1376pub fn read_wal_frame_checksum(frame_header: &[u8]) -> Result<SqliteWalChecksum> {
1378 ensure_min_len(frame_header, WAL_FRAME_HEADER_SIZE, "WAL frame header")?;
1379 Ok(SqliteWalChecksum {
1380 s1: read_be_u32_at(frame_header, WAL_FRAME_CKSUM1_OFFSET),
1381 s2: read_be_u32_at(frame_header, WAL_FRAME_CKSUM2_OFFSET),
1382 })
1383}
1384
1385pub fn compute_wal_frame_checksum(
1387 frame: &[u8],
1388 page_size: usize,
1389 previous: SqliteWalChecksum,
1390 big_endian_checksum_words: bool,
1391) -> Result<SqliteWalChecksum> {
1392 ensure_frame_len(frame, page_size)?;
1393 let c1 = sqlite_wal_checksum(
1394 &frame[..8],
1395 previous.s1,
1396 previous.s2,
1397 big_endian_checksum_words,
1398 )?;
1399 sqlite_wal_checksum(
1400 &frame[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + page_size],
1401 c1.s1,
1402 c1.s2,
1403 big_endian_checksum_words,
1404 )
1405}
1406
1407pub fn write_wal_frame_checksum(
1409 frame: &mut [u8],
1410 page_size: usize,
1411 previous: SqliteWalChecksum,
1412 big_endian_checksum_words: bool,
1413) -> Result<SqliteWalChecksum> {
1414 let checksum =
1415 compute_wal_frame_checksum(frame, page_size, previous, big_endian_checksum_words)?;
1416 write_wal_frame_checksum_fields(frame, checksum)?;
1417 Ok(checksum)
1418}
1419
1420pub fn write_wal_frame_checksum_fields(
1422 frame: &mut [u8],
1423 checksum: SqliteWalChecksum,
1424) -> Result<()> {
1425 ensure_min_len(frame, WAL_FRAME_HEADER_SIZE, "WAL frame")?;
1426 write_be_u32_at(frame, WAL_FRAME_CKSUM1_OFFSET, checksum.s1);
1427 write_be_u32_at(frame, WAL_FRAME_CKSUM2_OFFSET, checksum.s2);
1428 Ok(())
1429}
1430
1431pub fn wal_frame_db_size(frame_header: &[u8]) -> Result<u32> {
1433 ensure_min_len(frame_header, WAL_FRAME_HEADER_SIZE, "WAL frame header")?;
1434 Ok(read_be_u32_at(frame_header, WAL_FRAME_DB_SIZE_OFFSET))
1435}
1436
1437pub fn validate_wal_chain(
1439 wal_bytes: &[u8],
1440 page_size: usize,
1441 big_endian_checksum_words: bool,
1442) -> Result<WalChainValidation> {
1443 ensure_min_len(wal_bytes, WAL_HEADER_SIZE, "WAL bytes")?;
1444 ensure_valid_wal_page_size(page_size, "WAL page_size")?;
1445
1446 let frame_size = WAL_FRAME_HEADER_SIZE + page_size;
1447 let wal_header = &wal_bytes[..WAL_HEADER_SIZE];
1448 if !validate_wal_header_checksum(wal_header, big_endian_checksum_words)? {
1449 return Ok(WalChainValidation::from_core(
1450 false,
1451 0,
1452 0,
1453 Some(0),
1454 Some(WalChainInvalidReason::HeaderChecksumMismatch),
1455 None,
1456 frame_size,
1457 ));
1458 }
1459
1460 let header_salts = read_wal_header_salts(wal_header)?;
1461 let mut running_checksum = read_wal_header_checksum(wal_header)?;
1462
1463 let frames = &wal_bytes[WAL_HEADER_SIZE..];
1464 let full_frames = frames.len() / frame_size;
1465 let trailing_bytes = frames.len() % frame_size;
1466
1467 let mut valid_frames = 0_usize;
1468 let mut replayable_frames = 0_usize;
1469 let mut last_commit_frame = None;
1470
1471 for frame_index in 0..full_frames {
1472 let start = frame_index * frame_size;
1473 let frame = &frames[start..start + frame_size];
1474 let frame_header = &frame[..WAL_FRAME_HEADER_SIZE];
1475
1476 if read_wal_frame_salts(frame_header)? != header_salts {
1477 return Ok(WalChainValidation::from_core(
1478 false,
1479 valid_frames,
1480 replayable_frames,
1481 Some(frame_index),
1482 Some(WalChainInvalidReason::SaltMismatch),
1483 last_commit_frame,
1484 frame_size,
1485 ));
1486 }
1487
1488 let expected = compute_wal_frame_checksum(
1489 frame,
1490 page_size,
1491 running_checksum,
1492 big_endian_checksum_words,
1493 )?;
1494 let actual = read_wal_frame_checksum(frame_header)?;
1495 if actual != expected {
1496 return Ok(WalChainValidation::from_core(
1497 false,
1498 valid_frames,
1499 replayable_frames,
1500 Some(frame_index),
1501 Some(WalChainInvalidReason::FrameChecksumMismatch),
1502 last_commit_frame,
1503 frame_size,
1504 ));
1505 }
1506
1507 running_checksum = actual;
1508 valid_frames += 1;
1509
1510 if wal_frame_db_size(frame_header)? > 0 {
1511 last_commit_frame = Some(frame_index);
1512 replayable_frames = frame_index + 1;
1513 }
1514 }
1515
1516 if trailing_bytes != 0 {
1517 return Ok(WalChainValidation::from_core(
1518 false,
1519 valid_frames,
1520 replayable_frames,
1521 Some(valid_frames),
1522 Some(WalChainInvalidReason::TruncatedFrame),
1523 last_commit_frame,
1524 frame_size,
1525 ));
1526 }
1527
1528 Ok(WalChainValidation::from_core(
1529 true,
1530 valid_frames,
1531 replayable_frames,
1532 None,
1533 None,
1534 last_commit_frame,
1535 frame_size,
1536 ))
1537}
1538
1539pub fn sqlite_wal_checksum(
1541 data: &[u8],
1542 seed_s1: u32,
1543 seed_s2: u32,
1544 big_endian_checksum_words: bool,
1545) -> Result<SqliteWalChecksum> {
1546 if !data.len().is_multiple_of(8) {
1547 return Err(FrankenError::WalCorrupt {
1548 detail: format!(
1549 "WAL checksum input must be 8-byte aligned, got {} bytes",
1550 data.len()
1551 ),
1552 });
1553 }
1554
1555 let mut s1 = seed_s1;
1556 let mut s2 = seed_s2;
1557
1558 let (chunks, _rest) = data.as_chunks::<8>();
1559 for chunk in chunks {
1560 let x0 = decode_u32_words(&chunk[..4], big_endian_checksum_words);
1561 let x1 = decode_u32_words(&chunk[4..], big_endian_checksum_words);
1562
1563 s1 = s1.wrapping_add(x0).wrapping_add(s2);
1564 s2 = s2.wrapping_add(x1).wrapping_add(s1);
1565 }
1566
1567 Ok(SqliteWalChecksum { s1, s2 })
1568}
1569
1570#[must_use]
1572pub fn integrity_hash_xxh3_128(data: &[u8]) -> [u8; 16] {
1573 xxh3_128(data).to_le_bytes()
1574}
1575
1576#[must_use]
1578pub fn content_address_hash_128(data: &[u8]) -> [u8; 16] {
1579 let digest = blake3::hash(data);
1580 let mut out = [0_u8; 16];
1581 out.copy_from_slice(&digest.as_bytes()[..16]);
1582 out
1583}
1584
1585#[must_use]
1587pub fn crc32c_checksum(data: &[u8]) -> u32 {
1588 crc32c::crc32c(data)
1589}
1590
1591#[must_use]
1593pub fn tier_for_algorithm(algorithm: &str) -> Option<HashTier> {
1594 let normalized = algorithm.trim().to_ascii_lowercase();
1595 match normalized.as_str() {
1596 "xxh3_128" | "xxh3" => Some(HashTier::Integrity),
1597 "blake3_128" | "blake3" => Some(HashTier::ContentAddressing),
1598 "crc32c" => Some(HashTier::Protocol),
1599 _ => None,
1600 }
1601}
1602
1603fn is_valid_schema_sql(sql: &str) -> bool {
1604 let normalized = sql.trim_start().to_ascii_uppercase();
1605 normalized.starts_with("CREATE TABLE ")
1606 || normalized.starts_with("CREATE INDEX ")
1607 || normalized.starts_with("CREATE VIEW ")
1608 || normalized.starts_with("CREATE TRIGGER ")
1609 || normalized.starts_with("CREATE VIRTUAL TABLE ")
1610}
1611
1612fn sqlite_page_size_from_header(db_bytes: &[u8]) -> Option<usize> {
1613 if db_bytes.len() < SQLITE_DB_HEADER_SIZE {
1614 return None;
1615 }
1616 let raw = u16::from_be_bytes([db_bytes[16], db_bytes[17]]);
1617 let page_size = if raw == 1 { 65_536 } else { usize::from(raw) };
1618 Some(page_size)
1619}
1620
1621fn normalize_first_page_header_offsets(page: &mut [u8]) {
1622 if page.len() < 7 {
1623 return;
1624 }
1625
1626 let first_freeblock = u16::from_be_bytes([page[1], page[2]]);
1627 if first_freeblock >= SQLITE_DB_HEADER_SIZE_U16 {
1628 let adjusted = first_freeblock.saturating_sub(SQLITE_DB_HEADER_SIZE_U16);
1629 page[1..3].copy_from_slice(&adjusted.to_be_bytes());
1630 } else if first_freeblock != 0 {
1631 page[1..3].copy_from_slice(&u16::MAX.to_be_bytes());
1633 }
1634
1635 let cell_content_offset = u16::from_be_bytes([page[5], page[6]]);
1636 if cell_content_offset >= SQLITE_DB_HEADER_SIZE_U16 {
1637 let adjusted = cell_content_offset.saturating_sub(SQLITE_DB_HEADER_SIZE_U16);
1638 page[5..7].copy_from_slice(&adjusted.to_be_bytes());
1639 } else if cell_content_offset != 0 {
1640 page[5..7].copy_from_slice(&u16::MAX.to_be_bytes());
1642 }
1643}
1644
1645fn ensure_min_len(bytes: &[u8], minimum: usize, label: &str) -> Result<()> {
1646 if bytes.len() < minimum {
1647 return Err(FrankenError::WalCorrupt {
1648 detail: format!(
1649 "{label} too small: expected >= {minimum}, got {}",
1650 bytes.len()
1651 ),
1652 });
1653 }
1654 Ok(())
1655}
1656
1657fn ensure_frame_len(frame: &[u8], page_size: usize) -> Result<()> {
1658 ensure_valid_wal_page_size(page_size, "frame page_size")?;
1659 let frame_size = WAL_FRAME_HEADER_SIZE + page_size;
1660 ensure_min_len(frame, frame_size, "WAL frame")
1661}
1662
1663#[must_use]
1678pub fn wal_header_treated_as_empty(header_buf: &[u8; WAL_HEADER_SIZE]) -> bool {
1679 let magic = read_be_u32_at(header_buf, 0);
1680 if magic != WAL_MAGIC_LE && magic != WAL_MAGIC_BE {
1681 return true;
1682 }
1683 if ensure_valid_wal_header_page_size(read_be_u32_at(header_buf, 8)).is_err() {
1684 return true;
1685 }
1686 let big_endian = magic == WAL_MAGIC_BE;
1687 let (Ok(stored), Ok(expected)) = (
1688 read_wal_header_checksum(header_buf),
1689 wal_header_checksum(header_buf, big_endian),
1690 ) else {
1691 return true;
1692 };
1693 stored != expected
1694}
1695
1696fn ensure_valid_wal_header_page_size(page_size: u32) -> Result<()> {
1697 if PageSize::new(page_size).is_none() {
1698 return Err(FrankenError::WalCorrupt {
1699 detail: format!(
1700 "invalid WAL header page_size {page_size}; expected power-of-two in 512..=65536"
1701 ),
1702 });
1703 }
1704 Ok(())
1705}
1706
1707fn ensure_valid_wal_page_size(page_size: usize, label: &str) -> Result<()> {
1708 let Ok(page_size_u32) = u32::try_from(page_size) else {
1709 return Err(FrankenError::WalCorrupt {
1710 detail: format!("{label} {page_size} does not fit in u32"),
1711 });
1712 };
1713 if PageSize::new(page_size_u32).is_none() {
1714 return Err(FrankenError::WalCorrupt {
1715 detail: format!("invalid {label} {page_size}; expected power-of-two in 512..=65536"),
1716 });
1717 }
1718 Ok(())
1719}
1720
1721#[inline]
1722fn decode_u32_words(bytes: &[u8], big_endian_checksum_words: bool) -> u32 {
1723 let raw = bytes[..4].try_into().unwrap();
1724 if big_endian_checksum_words {
1725 u32::from_be_bytes(raw)
1726 } else {
1727 u32::from_le_bytes(raw)
1728 }
1729}
1730
1731#[inline]
1732fn read_be_u32_at(bytes: &[u8], offset: usize) -> u32 {
1733 u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap())
1734}
1735
1736fn write_be_u32_at(bytes: &mut [u8], offset: usize, value: u32) {
1737 bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
1738}
1739
1740fn from_u128_le(value: u128) -> Xxh3Checksum128 {
1741 let bytes = value.to_le_bytes();
1742 let mut low = [0_u8; 8];
1743 let mut high = [0_u8; 8];
1744 low.copy_from_slice(&bytes[..8]);
1745 high.copy_from_slice(&bytes[8..]);
1746 Xxh3Checksum128 {
1747 low: u64::from_le_bytes(low),
1748 high: u64::from_le_bytes(high),
1749 }
1750}
1751
1752fn read_xxh3_from_bytes(bytes: &[u8]) -> Xxh3Checksum128 {
1753 let mut low = [0_u8; 8];
1754 let mut high = [0_u8; 8];
1755 low.copy_from_slice(&bytes[..8]);
1756 high.copy_from_slice(&bytes[8..16]);
1757 Xxh3Checksum128 {
1758 low: u64::from_le_bytes(low),
1759 high: u64::from_le_bytes(high),
1760 }
1761}
1762
1763#[cfg(test)]
1764mod tests {
1765 use super::*;
1766
1767 const PAGE_SIZE: usize = 4096;
1768
1769 fn sample_page(seed: u8) -> [u8; PAGE_SIZE] {
1770 let mut page = [0_u8; PAGE_SIZE];
1771 for (index, byte) in page.iter_mut().enumerate() {
1772 let reduced_index = u8::try_from(index % 251).expect("modulo result must fit in u8");
1773 *byte = reduced_index ^ seed;
1774 }
1775 page
1776 }
1777
1778 fn sample_btree_leaf_page() -> [u8; PAGE_SIZE] {
1779 let mut page = [0_u8; PAGE_SIZE];
1780 page[0] = 0x0D; page[1..3].copy_from_slice(&0_u16.to_be_bytes()); page[3..5].copy_from_slice(&0_u16.to_be_bytes()); page[5..7].copy_from_slice(
1784 &u16::try_from(PAGE_SIZE)
1785 .expect("PAGE_SIZE should fit in u16 for test")
1786 .to_be_bytes(),
1787 );
1788 page[7] = 0; page
1790 }
1791
1792 #[test]
1793 fn test_wal_header_magic_le_roundtrip() {
1794 let header = WalHeader {
1795 magic: WAL_MAGIC_LE,
1796 format_version: WAL_FORMAT_VERSION,
1797 page_size: u32::try_from(PAGE_SIZE).expect("page size fits in u32"),
1798 checkpoint_seq: 7,
1799 salts: WalSalts {
1800 salt1: 0x1111_2222,
1801 salt2: 0x3333_4444,
1802 },
1803 checksum: SqliteWalChecksum::default(),
1804 };
1805 let bytes = header.to_bytes().expect("header should serialize");
1806 assert_eq!(read_be_u32_at(&bytes, 0), WAL_MAGIC_LE);
1807 assert!(
1808 validate_wal_header_checksum(&bytes, false).expect("header checksum should validate")
1809 );
1810
1811 let parsed = WalHeader::from_bytes(&bytes).expect("header should parse");
1812 assert_eq!(parsed.magic, WAL_MAGIC_LE);
1813 assert!(!parsed.big_endian_checksum());
1814 }
1815
1816 #[test]
1817 fn test_wal_header_magic_be_roundtrip() {
1818 let header = WalHeader {
1819 magic: WAL_MAGIC_BE,
1820 format_version: WAL_FORMAT_VERSION,
1821 page_size: u32::try_from(PAGE_SIZE).expect("page size fits in u32"),
1822 checkpoint_seq: 11,
1823 salts: WalSalts {
1824 salt1: 0xAAAA_BBBB,
1825 salt2: 0xCCCC_DDDD,
1826 },
1827 checksum: SqliteWalChecksum::default(),
1828 };
1829 let bytes = header.to_bytes().expect("header should serialize");
1830 assert_eq!(read_be_u32_at(&bytes, 0), WAL_MAGIC_BE);
1831 assert!(
1832 validate_wal_header_checksum(&bytes, true).expect("header checksum should validate")
1833 );
1834
1835 let parsed = WalHeader::from_bytes(&bytes).expect("header should parse");
1836 assert_eq!(parsed.magic, WAL_MAGIC_BE);
1837 assert!(parsed.big_endian_checksum());
1838 }
1839
1840 #[test]
1841 fn test_wal_header_format_version_constant_and_rejection() {
1842 assert_eq!(WAL_FORMAT_VERSION, 3_007_000);
1843
1844 let header = WalHeader {
1845 magic: WAL_MAGIC_LE,
1846 format_version: WAL_FORMAT_VERSION,
1847 page_size: u32::try_from(PAGE_SIZE).expect("page size fits in u32"),
1848 checkpoint_seq: 0,
1849 salts: WalSalts { salt1: 1, salt2: 2 },
1850 checksum: SqliteWalChecksum::default(),
1851 };
1852 let mut bytes = header.to_bytes().expect("header should serialize");
1853 write_be_u32_at(&mut bytes, 4, WAL_FORMAT_VERSION + 1);
1854 let err = WalHeader::from_bytes(&bytes).expect_err("invalid version must be rejected");
1855 assert!(matches!(err, FrankenError::WalCorrupt { .. }));
1856 }
1857
1858 #[test]
1859 fn test_wal_header_rejects_invalid_page_size_on_parse_and_serialize() {
1860 let header = WalHeader {
1861 magic: WAL_MAGIC_LE,
1862 format_version: WAL_FORMAT_VERSION,
1863 page_size: u32::try_from(PAGE_SIZE).expect("page size fits in u32"),
1864 checkpoint_seq: 0,
1865 salts: WalSalts { salt1: 1, salt2: 2 },
1866 checksum: SqliteWalChecksum::default(),
1867 };
1868
1869 let mut bytes = header.to_bytes().expect("valid header should serialize");
1870 write_be_u32_at(&mut bytes, 8, 3000);
1871 let parse_err =
1872 WalHeader::from_bytes(&bytes).expect_err("invalid page size must be rejected");
1873 assert!(matches!(parse_err, FrankenError::WalCorrupt { .. }));
1874
1875 let invalid_header = WalHeader {
1876 page_size: 3000,
1877 ..header
1878 };
1879 let serialize_err = invalid_header
1880 .to_bytes()
1881 .expect_err("invalid page size must not serialize");
1882 assert!(matches!(serialize_err, FrankenError::WalCorrupt { .. }));
1883 }
1884
1885 #[test]
1886 fn test_wal_frame_header_commit_and_non_commit() {
1887 let salts = WalSalts {
1888 salt1: 0x0102_0304,
1889 salt2: 0x0506_0708,
1890 };
1891 let checksum = SqliteWalChecksum {
1892 s1: 0x1111_1111,
1893 s2: 0x2222_2222,
1894 };
1895
1896 let non_commit = WalFrameHeader {
1897 page_number: 4,
1898 db_size: 0,
1899 salts,
1900 checksum,
1901 };
1902 assert!(!non_commit.is_commit());
1903 let parsed_non_commit =
1904 WalFrameHeader::from_bytes(&non_commit.to_bytes()).expect("frame should parse");
1905 assert_eq!(parsed_non_commit, non_commit);
1906
1907 let commit = WalFrameHeader {
1908 page_number: 5,
1909 db_size: 99,
1910 salts,
1911 checksum,
1912 };
1913 assert!(commit.is_commit());
1914 let parsed_commit =
1915 WalFrameHeader::from_bytes(&commit.to_bytes()).expect("frame should parse");
1916 assert_eq!(parsed_commit, commit);
1917 }
1918
1919 #[test]
1920 fn test_wal_frame_salt_match_validation() {
1921 let header = WalHeader {
1922 magic: WAL_MAGIC_LE,
1923 format_version: WAL_FORMAT_VERSION,
1924 page_size: u32::try_from(PAGE_SIZE).expect("page size fits in u32"),
1925 checkpoint_seq: 1,
1926 salts: WalSalts {
1927 salt1: 0xABCD_1234,
1928 salt2: 0x9876_5432,
1929 },
1930 checksum: SqliteWalChecksum::default(),
1931 };
1932 let header_bytes = header.to_bytes().expect("header should serialize");
1933 let seed = read_wal_header_checksum(&header_bytes).expect("header checksum should read");
1934
1935 let mut frame = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
1936 frame[..4].copy_from_slice(&1_u32.to_be_bytes());
1937 frame[4..8].copy_from_slice(&1_u32.to_be_bytes());
1938 write_wal_frame_salts(&mut frame[..WAL_FRAME_HEADER_SIZE], header.salts)
1939 .expect("frame salts should write");
1940 frame[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&sample_page(0x3A));
1941 write_wal_frame_checksum(&mut frame, PAGE_SIZE, seed, false)
1942 .expect("frame checksum should write");
1943
1944 let mut wal_bytes = Vec::with_capacity(WAL_HEADER_SIZE + frame.len());
1945 wal_bytes.extend_from_slice(&header_bytes);
1946 wal_bytes.extend_from_slice(&frame);
1947 let valid = validate_wal_chain(&wal_bytes, PAGE_SIZE, false).expect("valid chain");
1948 assert!(valid.valid);
1949 assert_eq!(valid.valid_frames, 1);
1950
1951 write_wal_frame_salts(
1952 &mut wal_bytes[WAL_HEADER_SIZE..WAL_HEADER_SIZE + WAL_FRAME_HEADER_SIZE],
1953 WalSalts {
1954 salt1: 0xDEAD_BEEF,
1955 salt2: 0xFACE_FEED,
1956 },
1957 )
1958 .expect("salt rewrite should succeed");
1959 let invalid =
1960 validate_wal_chain(&wal_bytes, PAGE_SIZE, false).expect("invalid chain should parse");
1961 assert_eq!(invalid.reason, Some(WalChainInvalidReason::SaltMismatch));
1962 assert_eq!(invalid.first_invalid_frame, Some(0));
1963 }
1964
1965 #[test]
1966 fn test_wal_checksum_transform_matches_frame_checksum() {
1967 let header = WalHeader {
1968 magic: WAL_MAGIC_LE,
1969 format_version: WAL_FORMAT_VERSION,
1970 page_size: u32::try_from(PAGE_SIZE).expect("page size fits in u32"),
1971 checkpoint_seq: 5,
1972 salts: WalSalts {
1973 salt1: 0x1234_5678,
1974 salt2: 0x9ABC_DEF0,
1975 },
1976 checksum: SqliteWalChecksum::default(),
1977 };
1978 let header_bytes = header.to_bytes().expect("header should serialize");
1979 let seed = read_wal_header_checksum(&header_bytes).expect("header checksum should read");
1980
1981 let mut frame = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
1982 frame[..4].copy_from_slice(&7_u32.to_be_bytes());
1983 frame[4..8].copy_from_slice(&7_u32.to_be_bytes());
1984 write_wal_frame_salts(&mut frame[..WAL_FRAME_HEADER_SIZE], header.salts)
1985 .expect("frame salts should write");
1986 frame[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&sample_page(0x55));
1987
1988 let transform =
1989 WalChecksumTransform::for_wal_frame(&frame, PAGE_SIZE, false).expect("transform");
1990 let transformed = transform.apply(seed);
1991 let computed =
1992 compute_wal_frame_checksum(&frame, PAGE_SIZE, seed, false).expect("checksum");
1993
1994 assert_eq!(
1995 transformed, computed,
1996 "precomputed frame transform must match direct checksum evaluation"
1997 );
1998 }
1999
2000 #[test]
2001 fn test_wal_checksum_transform_matches_direct_checksum_for_chunk_counts() {
2002 for big_endian in [false, true] {
2003 for chunk_count in [0_usize, 1, 2, 3, 8, 31, 512] {
2004 let mut data = vec![0_u8; chunk_count * 8];
2005 for (idx, byte) in data.iter_mut().enumerate() {
2006 *byte = u8::try_from((idx * 37 + chunk_count * 11) & 0xFF)
2007 .expect("masked byte fits");
2008 }
2009 let transform = WalChecksumTransform::from_aligned_bytes(&data, big_endian)
2010 .expect("aligned transform should build");
2011 for seed in [
2012 SqliteWalChecksum { s1: 0, s2: 0 },
2013 SqliteWalChecksum {
2014 s1: 0x1234_5678,
2015 s2: 0x9ABC_DEF0,
2016 },
2017 SqliteWalChecksum {
2018 s1: u32::MAX,
2019 s2: 0x0102_0304,
2020 },
2021 ] {
2022 let direct = sqlite_wal_checksum(&data, seed.s1, seed.s2, big_endian)
2023 .expect("aligned checksum should compute");
2024 assert_eq!(
2025 transform.apply(seed),
2026 direct,
2027 "transform must match direct checksum for big_endian={big_endian} chunk_count={chunk_count} seed={seed:?}",
2028 );
2029 }
2030 }
2031 }
2032 }
2033
2034 #[test]
2035 fn test_wal_checksum_chain_integrity_two_frames() {
2036 let header = WalHeader {
2037 magic: WAL_MAGIC_LE,
2038 format_version: WAL_FORMAT_VERSION,
2039 page_size: u32::try_from(PAGE_SIZE).expect("page size fits in u32"),
2040 checkpoint_seq: 3,
2041 salts: WalSalts {
2042 salt1: 0xA1A2_A3A4,
2043 salt2: 0xB1B2_B3B4,
2044 },
2045 checksum: SqliteWalChecksum::default(),
2046 };
2047 let header_bytes = header.to_bytes().expect("header should serialize");
2048 let mut running_checksum =
2049 read_wal_header_checksum(&header_bytes).expect("header checksum should read");
2050
2051 let mut frame1 = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
2052 frame1[..4].copy_from_slice(&1_u32.to_be_bytes());
2053 frame1[4..8].copy_from_slice(&0_u32.to_be_bytes());
2054 write_wal_frame_salts(&mut frame1[..WAL_FRAME_HEADER_SIZE], header.salts)
2055 .expect("frame salts should write");
2056 frame1[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&sample_page(0x10));
2057 running_checksum =
2058 write_wal_frame_checksum(&mut frame1, PAGE_SIZE, running_checksum, false)
2059 .expect("frame checksum should write");
2060
2061 let mut frame2 = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
2062 frame2[..4].copy_from_slice(&2_u32.to_be_bytes());
2063 frame2[4..8].copy_from_slice(&7_u32.to_be_bytes());
2064 write_wal_frame_salts(&mut frame2[..WAL_FRAME_HEADER_SIZE], header.salts)
2065 .expect("frame salts should write");
2066 frame2[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&sample_page(0x20));
2067 let frame2_checksum =
2068 write_wal_frame_checksum(&mut frame2, PAGE_SIZE, running_checksum, false)
2069 .expect("frame checksum should write");
2070
2071 let mut wal_bytes = Vec::with_capacity(WAL_HEADER_SIZE + frame1.len() + frame2.len());
2072 wal_bytes.extend_from_slice(&header_bytes);
2073 wal_bytes.extend_from_slice(&frame1);
2074 wal_bytes.extend_from_slice(&frame2);
2075 let validation = validate_wal_chain(&wal_bytes, PAGE_SIZE, false).expect("valid chain");
2076
2077 assert!(validation.valid);
2078 assert_eq!(validation.valid_frames, 2);
2079 assert_eq!(validation.replayable_frames, 2);
2080 assert_eq!(validation.last_commit_frame, Some(1));
2081 let parsed_frame2 =
2082 WalFrameHeader::from_bytes(&frame2[..WAL_FRAME_HEADER_SIZE]).expect("frame parses");
2083 assert_eq!(parsed_frame2.checksum, frame2_checksum);
2084 }
2085
2086 #[test]
2087 fn test_wal_frame_checksum_ignores_salt_words() {
2088 let seed = SqliteWalChecksum {
2089 s1: 0x1234_5678,
2090 s2: 0x9ABC_DEF0,
2091 };
2092 let mut frame_a = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
2093 frame_a[..4].copy_from_slice(&2_u32.to_be_bytes());
2094 frame_a[4..8].copy_from_slice(&0_u32.to_be_bytes());
2095 write_wal_frame_salts(
2096 &mut frame_a[..WAL_FRAME_HEADER_SIZE],
2097 WalSalts { salt1: 1, salt2: 2 },
2098 )
2099 .expect("frame salts should write");
2100 frame_a[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&sample_page(0x55));
2101
2102 let mut frame_b = frame_a.clone();
2103 write_wal_frame_salts(
2104 &mut frame_b[..WAL_FRAME_HEADER_SIZE],
2105 WalSalts {
2106 salt1: 0xAAAA_BBBB,
2107 salt2: 0xCCCC_DDDD,
2108 },
2109 )
2110 .expect("frame salts should write");
2111
2112 let checksum_a =
2113 compute_wal_frame_checksum(&frame_a, PAGE_SIZE, seed, false).expect("checksum");
2114 let checksum_b =
2115 compute_wal_frame_checksum(&frame_b, PAGE_SIZE, seed, false).expect("checksum");
2116 assert_eq!(checksum_a, checksum_b);
2117 }
2118
2119 #[test]
2120 fn test_sqlite_checksum_alignment_guard() {
2121 let err = sqlite_wal_checksum(&[1_u8, 2, 3], 0, 0, false).expect_err("must reject");
2122 assert!(matches!(err, FrankenError::WalCorrupt { .. }));
2123 let detail = match err {
2124 FrankenError::WalCorrupt { detail } => detail,
2125 _ => String::new(),
2126 };
2127 assert!(detail.contains("8-byte aligned"));
2128 }
2129
2130 #[test]
2131 fn test_page_checksum_roundtrip() {
2132 let mut page = sample_page(7);
2133 let expected = write_page_checksum(&mut page).expect("write should succeed");
2134 let actual = read_page_checksum(&page).expect("read should succeed");
2135 assert_eq!(expected, actual);
2136 assert!(verify_page_checksum(&page).expect("verify should succeed"));
2137 }
2138
2139 #[test]
2140 fn test_configure_reserved_bytes() {
2141 let mut header = [0_u8; 100];
2142 configure_page_checksum_reserved_bytes(&mut header, true).expect("config should work");
2143 assert_eq!(
2144 page_checksum_reserved_bytes(&header).expect("read should work"),
2145 u8::try_from(PAGE_CHECKSUM_RESERVED_BYTES).expect("fits")
2146 );
2147 }
2148
2149 #[test]
2150 fn test_integrity_check_database_header_magic() {
2151 let mut bytes = vec![0_u8; SQLITE_DB_HEADER_SIZE];
2152 bytes[..SQLITE_DB_HEADER_MAGIC.len()].copy_from_slice(&SQLITE_DB_HEADER_MAGIC);
2153 bytes[16..18].copy_from_slice(&4096_u16.to_be_bytes());
2154 let ok_report = integrity_check_database_header(&bytes);
2155 assert!(ok_report.is_ok());
2156
2157 bytes[0] ^= 0x7F;
2158 let bad_report = integrity_check_database_header(&bytes);
2159 assert!(
2160 bad_report
2161 .sqlite_messages()
2162 .iter()
2163 .any(|line| line.contains("header magic mismatch"))
2164 );
2165 }
2166
2167 #[test]
2168 fn test_integrity_check_valid_db() {
2169 let page = sample_btree_leaf_page();
2170 let report = integrity_check_level1_page(&page, 1, true, false)
2171 .expect("level1 integrity check should run");
2172 assert!(report.is_ok());
2173 assert_eq!(report.sqlite_messages(), vec!["ok".to_owned()]);
2174 }
2175
2176 #[test]
2177 fn test_integrity_check_bad_page_type() {
2178 let mut page = sample_btree_leaf_page();
2179 page[0] = 0xFF;
2180
2181 let report = integrity_check_level1_page(&page, 7, true, false)
2182 .expect("level1 integrity check should run");
2183 assert!(!report.is_ok());
2184 assert!(
2185 report
2186 .sqlite_messages()
2187 .iter()
2188 .any(|line| line.contains("invalid b-tree page type"))
2189 );
2190 }
2191
2192 #[test]
2193 fn test_integrity_check_overlapping_cells() {
2194 let report =
2195 integrity_check_level2_btree(11, PAGE_SIZE, &[(100, 220), (200, 280)], &[1, 2]);
2196 assert!(
2197 report
2198 .sqlite_messages()
2199 .iter()
2200 .any(|line| line.contains("overlapping cell spans"))
2201 );
2202 }
2203
2204 #[test]
2205 fn test_integrity_check_unsorted_keys() {
2206 let report =
2207 integrity_check_level2_btree(12, PAGE_SIZE, &[(100, 120), (140, 180)], &[1, 3, 2]);
2208 assert!(
2209 report
2210 .sqlite_messages()
2211 .iter()
2212 .any(|line| line.contains("keys out of order"))
2213 );
2214 }
2215
2216 #[test]
2217 fn test_integrity_check_bad_overflow() {
2218 let report = integrity_check_level3_overflow_chain(13, &[7, 8, 7], 64);
2219 assert!(
2220 report
2221 .sqlite_messages()
2222 .iter()
2223 .any(|line| line.contains("broken overflow chain"))
2224 );
2225 }
2226
2227 #[test]
2228 fn test_integrity_check_page_not_accounted() {
2229 let report = integrity_check_level4_cross_reference(4, &[1, 3, 4]);
2230 assert!(
2231 report
2232 .sqlite_messages()
2233 .iter()
2234 .any(|line| line.contains("page 2: not accounted"))
2235 );
2236 }
2237
2238 #[test]
2239 fn test_integrity_check_schema_corrupt() {
2240 let report = integrity_check_level5_schema(&["garbage schema line".to_owned()]);
2241 assert!(
2242 report
2243 .sqlite_messages()
2244 .iter()
2245 .any(|line| line.contains("malformed SQL entry"))
2246 );
2247 }
2248
2249 #[test]
2250 fn test_integrity_check_output_matches_c() {
2251 let level1 = integrity_check_level1_page(&sample_btree_leaf_page(), 1, true, false)
2252 .expect("level1 integrity check should run");
2253 let level2 =
2254 integrity_check_level2_btree(1, PAGE_SIZE, &[(120, 140), (220, 250)], &[1, 2, 3]);
2255 let level3 = integrity_check_level3_overflow_chain(1, &[7, 9, 11], 20);
2256 let level4 = integrity_check_level4_cross_reference(3, &[1, 2, 3]);
2257 let level5 = integrity_check_level5_schema(&["CREATE TABLE t(x INTEGER)".to_owned()]);
2258 let report = merge_integrity_reports(&[level1, level2, level3, level4, level5]);
2259 assert_eq!(report.sqlite_messages(), vec!["ok".to_owned()]);
2260 }
2261
2262 #[test]
2263 fn test_recovery_wal_fec_repair() {
2264 let action = recovery_action_for_checksum_failure(
2265 ChecksumFailureKind::WalFrameChecksumMismatch,
2266 Some(8),
2267 Some(6),
2268 );
2269 assert_eq!(action, RecoveryAction::AttemptWalFecRepair);
2270
2271 let payload = sample_page(11);
2272 let hash = wal_fec_source_hash_xxh3_128(&payload);
2273 let decision = recover_wal_frame_checksum_mismatch(Some(&payload), Some(hash), 8, 6);
2274 assert_eq!(decision, WalRecoveryDecision::Repaired);
2275 }
2276
2277 #[test]
2278 fn test_recovery_wal_fec_insufficient() {
2279 let action = recovery_action_for_checksum_failure(
2280 ChecksumFailureKind::WalFrameChecksumMismatch,
2281 Some(3),
2282 Some(4),
2283 );
2284 assert_eq!(action, RecoveryAction::TruncateWalAtFirstInvalidFrame);
2285
2286 let payload = sample_page(9);
2287 let hash = wal_fec_source_hash_xxh3_128(&payload);
2288 let decision = recover_wal_frame_checksum_mismatch(Some(&payload), Some(hash), 3, 4);
2289 assert_eq!(decision, WalRecoveryDecision::Truncated);
2290 }
2291
2292 #[test]
2293 fn test_recovery_crc32c_exclude() {
2294 let action = recovery_action_for_checksum_failure(
2295 ChecksumFailureKind::Crc32cSymbolMismatch,
2296 Some(0),
2297 Some(0),
2298 );
2299 assert_eq!(action, RecoveryAction::ExcludeCorruptedSymbolAndContinue);
2300 }
2301
2302 #[test]
2303 fn test_recovery_xxh3_evict_retry() {
2304 let action = recovery_action_for_checksum_failure(
2305 ChecksumFailureKind::Xxh3PageChecksumMismatch,
2306 None,
2307 None,
2308 );
2309 assert_eq!(action, RecoveryAction::EvictCacheAndRetryFromWal);
2310 }
2311
2312 #[test]
2313 fn test_recovery_wal_fec_hash_mismatch_truncates() {
2314 let payload = sample_page(3);
2315 let wrong_hash = wal_fec_source_hash_xxh3_128(&sample_page(4));
2316 let decision = recover_wal_frame_checksum_mismatch(Some(&payload), Some(wrong_hash), 8, 6);
2317 assert_eq!(decision, WalRecoveryDecision::Truncated);
2318 }
2319
2320 #[test]
2321 fn test_crash_at_any_point() {
2322 let contract = crash_model_contract();
2323 assert!(contract.crash_at_any_point());
2324 for crash_step in 0..16 {
2325 let before = u64::try_from(crash_step).expect("step should fit");
2326 let after = before.saturating_add(1);
2327 assert!(after >= before);
2328 assert!(contract.fsync_is_durability_barrier());
2329 }
2330 }
2331
2332 #[test]
2333 fn test_torn_write_detection() {
2334 let mut wal_header = [0_u8; WAL_HEADER_SIZE];
2335 wal_header[..4].copy_from_slice(&0x377F_0682_u32.to_be_bytes());
2336 wal_header[4..8].copy_from_slice(&3_007_000_u32.to_be_bytes());
2337 wal_header[8..12].copy_from_slice(
2338 &u32::try_from(PAGE_SIZE)
2339 .expect("PAGE_SIZE should fit in u32")
2340 .to_be_bytes(),
2341 );
2342 let salts = WalSalts {
2343 salt1: 0x1111_2222,
2344 salt2: 0x3333_4444,
2345 };
2346 write_wal_header_salts(&mut wal_header, salts).expect("header salts should write");
2347 write_wal_header_checksum(&mut wal_header, false).expect("header checksum should write");
2348
2349 let mut frame = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
2350 frame[..4].copy_from_slice(&1_u32.to_be_bytes());
2351 frame[4..8].copy_from_slice(&1_u32.to_be_bytes());
2352 write_wal_frame_salts(&mut frame[..WAL_FRAME_HEADER_SIZE], salts)
2353 .expect("frame salts should write");
2354
2355 for (idx, byte) in frame[WAL_FRAME_HEADER_SIZE..].iter_mut().enumerate() {
2356 let reduced = u8::try_from(idx % 251).expect("index modulo fits in u8");
2357 *byte = reduced ^ 0x5A;
2358 }
2359
2360 let seed = read_wal_header_checksum(&wal_header).expect("header checksum should read");
2361 write_wal_frame_checksum(&mut frame, PAGE_SIZE, seed, false)
2362 .expect("frame checksum should write");
2363
2364 let mut wal_bytes = Vec::with_capacity(WAL_HEADER_SIZE + frame.len());
2365 wal_bytes.extend_from_slice(&wal_header);
2366 wal_bytes.extend_from_slice(&frame);
2367 assert!(!detect_torn_write_in_wal(&wal_bytes, PAGE_SIZE, false).expect("validate WAL"));
2368
2369 wal_bytes.truncate(WAL_HEADER_SIZE + WAL_FRAME_HEADER_SIZE + PAGE_SIZE / 2);
2370 assert!(detect_torn_write_in_wal(&wal_bytes, PAGE_SIZE, false).expect("validate torn WAL"));
2371 }
2372
2373 #[test]
2374 fn test_fsync_durability() {
2375 let contract = crash_model_contract();
2376 assert!(contract.crash_at_any_point());
2377 assert!(contract.fsync_is_durability_barrier());
2378 assert!(contract.writes_reorder_without_fsync());
2379 assert!(contract.bitrot_exists());
2380 assert!(contract.metadata_may_require_directory_fsync());
2381 assert!(supports_torn_write_sector_size(512));
2382 assert!(supports_torn_write_sector_size(1024));
2383 assert!(supports_torn_write_sector_size(4096));
2384 assert!(!supports_torn_write_sector_size(2048));
2385 }
2386
2387 #[test]
2388 fn test_e2e_bd_36hc() {
2389 let level1 = integrity_check_level1_page(&sample_btree_leaf_page(), 1, true, false)
2390 .expect("level1 integrity check should run");
2391 let level2 = integrity_check_level2_btree(
2392 1,
2393 PAGE_SIZE,
2394 &[(120, 150), (180, 210), (240, 280)],
2395 &[1, 2, 3],
2396 );
2397 let level3 = integrity_check_level3_overflow_chain(1, &[5, 7, 9], 64);
2398 let level4 = integrity_check_level4_cross_reference(10, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
2399 let level5 = integrity_check_level5_schema(&[
2400 "CREATE TABLE t0(id INTEGER PRIMARY KEY, v TEXT)".to_owned(),
2401 "CREATE INDEX i0 ON t0(v)".to_owned(),
2402 ]);
2403 let merged = merge_integrity_reports(&[level1, level2, level3, level4, level5]);
2404
2405 assert!(merged.is_ok());
2406 assert_eq!(merged.sqlite_messages(), vec!["ok".to_owned()]);
2407
2408 let mut wal_header = [0_u8; WAL_HEADER_SIZE];
2409 wal_header[..4].copy_from_slice(&0x377F_0682_u32.to_be_bytes());
2410 wal_header[4..8].copy_from_slice(&3_007_000_u32.to_be_bytes());
2411 wal_header[8..12].copy_from_slice(
2412 &u32::try_from(PAGE_SIZE)
2413 .expect("PAGE_SIZE should fit in u32")
2414 .to_be_bytes(),
2415 );
2416 let salts = WalSalts {
2417 salt1: 0x0102_0304,
2418 salt2: 0xA0B0_C0D0,
2419 };
2420 write_wal_header_salts(&mut wal_header, salts).expect("header salts should write");
2421 write_wal_header_checksum(&mut wal_header, false).expect("header checksum should write");
2422 let mut running =
2423 read_wal_header_checksum(&wal_header).expect("header checksum should read");
2424
2425 let mut wal_bytes =
2426 Vec::with_capacity(WAL_HEADER_SIZE + 100 * (WAL_FRAME_HEADER_SIZE + PAGE_SIZE));
2427 wal_bytes.extend_from_slice(&wal_header);
2428 for frame_index in 0..100_u32 {
2429 let mut frame = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
2430 frame[..4].copy_from_slice(&(frame_index + 1).to_be_bytes());
2431 frame[4..8].copy_from_slice(&(frame_index + 1).to_be_bytes()); write_wal_frame_salts(&mut frame[..WAL_FRAME_HEADER_SIZE], salts)
2433 .expect("frame salts should write");
2434 for (offset, byte) in frame[WAL_FRAME_HEADER_SIZE..].iter_mut().enumerate() {
2435 let reduced = u8::try_from(offset % 251).expect("offset modulo must fit");
2436 *byte = reduced ^ u8::try_from(frame_index % 251).expect("frame modulo must fit");
2437 }
2438 running = write_wal_frame_checksum(&mut frame, PAGE_SIZE, running, false)
2439 .expect("frame checksum should write");
2440 wal_bytes.extend_from_slice(&frame);
2441 }
2442
2443 for scenario in 0..100_usize {
2444 let crash_frame = (scenario * 37) % 100;
2445 let torn_cut = WAL_HEADER_SIZE
2446 + crash_frame * (WAL_FRAME_HEADER_SIZE + PAGE_SIZE)
2447 + WAL_FRAME_HEADER_SIZE
2448 + PAGE_SIZE / 3;
2449 let torn = &wal_bytes[..torn_cut];
2450 let validation =
2451 validate_wal_chain(torn, PAGE_SIZE, false).expect("torn chain should parse");
2452 assert_eq!(validation.valid_frames, crash_frame);
2453 assert_eq!(validation.replayable_frames, crash_frame);
2454 }
2455 }
2456
2457 #[test]
2460 fn test_wal_header_magic_le() {
2461 let header = WalHeader {
2462 magic: WAL_MAGIC_LE,
2463 format_version: WAL_FORMAT_VERSION,
2464 page_size: 4096,
2465 checkpoint_seq: 0,
2466 salts: WalSalts {
2467 salt1: 0xAAAA_BBBB,
2468 salt2: 0xCCCC_DDDD,
2469 },
2470 checksum: SqliteWalChecksum::default(),
2471 };
2472 assert!(!header.big_endian_checksum());
2473 let bytes = header.to_bytes().expect("LE header should serialize");
2474 assert_eq!(&bytes[..4], &WAL_MAGIC_LE.to_be_bytes());
2475 }
2476
2477 #[test]
2478 fn test_wal_header_magic_be() {
2479 let header = WalHeader {
2480 magic: WAL_MAGIC_BE,
2481 format_version: WAL_FORMAT_VERSION,
2482 page_size: 4096,
2483 checkpoint_seq: 0,
2484 salts: WalSalts {
2485 salt1: 0x1111_2222,
2486 salt2: 0x3333_4444,
2487 },
2488 checksum: SqliteWalChecksum::default(),
2489 };
2490 assert!(header.big_endian_checksum());
2491 let bytes = header.to_bytes().expect("BE header should serialize");
2492 assert_eq!(&bytes[..4], &WAL_MAGIC_BE.to_be_bytes());
2493 }
2494
2495 #[test]
2496 fn test_wal_header_format_version() {
2497 let header = WalHeader {
2498 magic: WAL_MAGIC_LE,
2499 format_version: WAL_FORMAT_VERSION,
2500 page_size: 4096,
2501 checkpoint_seq: 1,
2502 salts: WalSalts::default(),
2503 checksum: SqliteWalChecksum::default(),
2504 };
2505 let bytes = header.to_bytes().expect("header should serialize");
2506 let parsed = WalHeader::from_bytes(&bytes).expect("header should parse");
2507 assert_eq!(parsed.format_version, 3_007_000);
2508
2509 let mut bad_bytes = bytes;
2511 bad_bytes[4..8].copy_from_slice(&999_u32.to_be_bytes());
2512 assert!(WalHeader::from_bytes(&bad_bytes).is_err());
2513 }
2514
2515 #[test]
2516 fn test_wal_header_round_trip() {
2517 let header = WalHeader {
2518 magic: WAL_MAGIC_LE,
2519 format_version: WAL_FORMAT_VERSION,
2520 page_size: 4096,
2521 checkpoint_seq: 42,
2522 salts: WalSalts {
2523 salt1: 0xDEAD_BEEF,
2524 salt2: 0xCAFE_BABE,
2525 },
2526 checksum: SqliteWalChecksum::default(),
2527 };
2528 let bytes = header.to_bytes().expect("header should serialize");
2529 assert_eq!(bytes.len(), WAL_HEADER_SIZE);
2530
2531 let parsed = WalHeader::from_bytes(&bytes).expect("header should parse");
2532 assert_eq!(parsed.magic, WAL_MAGIC_LE);
2533 assert_eq!(parsed.format_version, WAL_FORMAT_VERSION);
2534 assert_eq!(parsed.page_size, 4096);
2535 assert_eq!(parsed.checkpoint_seq, 42);
2536 assert_eq!(parsed.salts.salt1, 0xDEAD_BEEF);
2537 assert_eq!(parsed.salts.salt2, 0xCAFE_BABE);
2538 assert!(
2540 parsed.checksum.s1 != 0 || parsed.checksum.s2 != 0,
2541 "computed checksum should be non-trivial"
2542 );
2543 }
2544
2545 #[test]
2546 fn test_wal_frame_header_commit() {
2547 let commit_frame = WalFrameHeader {
2549 page_number: 1,
2550 db_size: 10,
2551 salts: WalSalts {
2552 salt1: 0x1111,
2553 salt2: 0x2222,
2554 },
2555 checksum: SqliteWalChecksum { s1: 100, s2: 200 },
2556 };
2557 assert!(commit_frame.is_commit());
2558
2559 let bytes = commit_frame.to_bytes();
2560 assert_eq!(bytes.len(), WAL_FRAME_HEADER_SIZE);
2561 let parsed = WalFrameHeader::from_bytes(&bytes).expect("frame should parse");
2562 assert!(parsed.is_commit());
2563 assert_eq!(parsed.db_size, 10);
2564
2565 let non_commit = WalFrameHeader {
2567 page_number: 2,
2568 db_size: 0,
2569 salts: WalSalts {
2570 salt1: 0x1111,
2571 salt2: 0x2222,
2572 },
2573 checksum: SqliteWalChecksum { s1: 300, s2: 400 },
2574 };
2575 assert!(!non_commit.is_commit());
2576
2577 let bytes2 = non_commit.to_bytes();
2578 let parsed2 = WalFrameHeader::from_bytes(&bytes2).expect("frame should parse");
2579 assert!(!parsed2.is_commit());
2580 assert_eq!(parsed2.db_size, 0);
2581 }
2582
2583 #[test]
2584 fn test_wal_frame_header_salt_match() {
2585 let wal_salts = WalSalts {
2586 salt1: 0xAAAA_BBBB,
2587 salt2: 0xCCCC_DDDD,
2588 };
2589
2590 let good_frame = WalFrameHeader {
2592 page_number: 1,
2593 db_size: 5,
2594 salts: wal_salts,
2595 checksum: SqliteWalChecksum::default(),
2596 };
2597 assert_eq!(good_frame.salts, wal_salts);
2598
2599 let bad_salts = WalSalts {
2601 salt1: 0x0000_0000,
2602 salt2: 0x0000_0000,
2603 };
2604 let bad_frame = WalFrameHeader {
2605 page_number: 1,
2606 db_size: 5,
2607 salts: bad_salts,
2608 checksum: SqliteWalChecksum::default(),
2609 };
2610 assert_ne!(
2611 bad_frame.salts, wal_salts,
2612 "mismatched salt must be detected"
2613 );
2614 }
2615
2616 #[test]
2617 fn test_wal_checksum_chain_integrity() {
2618 let salts = WalSalts {
2620 salt1: 0x1234_5678,
2621 salt2: 0x9ABC_DEF0,
2622 };
2623 let mut header_buf = [0_u8; WAL_HEADER_SIZE];
2624 header_buf[..4].copy_from_slice(&WAL_MAGIC_LE.to_be_bytes());
2625 header_buf[4..8].copy_from_slice(&WAL_FORMAT_VERSION.to_be_bytes());
2626 header_buf[8..12].copy_from_slice(
2627 &u32::try_from(PAGE_SIZE)
2628 .expect("page size fits")
2629 .to_be_bytes(),
2630 );
2631 write_wal_header_salts(&mut header_buf, salts).expect("write salts");
2632 write_wal_header_checksum(&mut header_buf, false).expect("write header checksum");
2633
2634 let mut running = read_wal_header_checksum(&header_buf).expect("read header checksum");
2635
2636 let mut wal_bytes = Vec::new();
2637 wal_bytes.extend_from_slice(&header_buf);
2638
2639 for frame_idx in 0..5_u32 {
2641 let mut frame = vec![0_u8; WAL_FRAME_HEADER_SIZE + PAGE_SIZE];
2642 frame[..4].copy_from_slice(&(frame_idx + 1).to_be_bytes());
2643 frame[4..8].copy_from_slice(&(frame_idx + 1).to_be_bytes()); write_wal_frame_salts(&mut frame[..WAL_FRAME_HEADER_SIZE], salts)
2645 .expect("write frame salts");
2646
2647 for (offset, byte) in frame[WAL_FRAME_HEADER_SIZE..].iter_mut().enumerate() {
2648 let r = u8::try_from(offset % 251).unwrap();
2649 let s = u8::try_from(frame_idx % 251).unwrap();
2650 *byte = r ^ s;
2651 }
2652
2653 running = write_wal_frame_checksum(&mut frame, PAGE_SIZE, running, false)
2654 .expect("write frame checksum");
2655 wal_bytes.extend_from_slice(&frame);
2656 }
2657
2658 let validation =
2660 validate_wal_chain(&wal_bytes, PAGE_SIZE, false).expect("chain should validate");
2661 assert!(validation.valid, "chain must be fully valid");
2662 assert_eq!(validation.valid_frames, 5);
2663 assert_eq!(validation.replayable_frames, 5);
2664 assert!(validation.reason.is_none());
2665
2666 let frame3_page_offset =
2668 WAL_HEADER_SIZE + 2 * (WAL_FRAME_HEADER_SIZE + PAGE_SIZE) + WAL_FRAME_HEADER_SIZE + 10;
2669 wal_bytes[frame3_page_offset] ^= 0xFF;
2670 let bad_validation =
2671 validate_wal_chain(&wal_bytes, PAGE_SIZE, false).expect("corrupt chain should parse");
2672 assert!(!bad_validation.valid);
2673 assert_eq!(bad_validation.valid_frames, 2, "frames 1-2 should be valid");
2674 assert_eq!(
2675 bad_validation.reason,
2676 Some(WalChainInvalidReason::FrameChecksumMismatch)
2677 );
2678 }
2679
2680 fn build_valid_wal(n: usize) -> Vec<u8> {
2684 let salts = WalSalts {
2685 salt1: 0xAAAA_BBBB,
2686 salt2: 0xCCCC_DDDD,
2687 };
2688 let mut header_buf = [0u8; WAL_HEADER_SIZE];
2689 header_buf[..4].copy_from_slice(&WAL_MAGIC_LE.to_be_bytes());
2690 header_buf[4..8].copy_from_slice(&WAL_FORMAT_VERSION.to_be_bytes());
2691 header_buf[8..12].copy_from_slice(
2692 &u32::try_from(PAGE_SIZE)
2693 .expect("page size fits")
2694 .to_be_bytes(),
2695 );
2696 write_wal_header_salts(&mut header_buf, salts).expect("write salts");
2697 write_wal_header_checksum(&mut header_buf, false).expect("write hdr cksum");
2698
2699 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
2700 let mut wal = Vec::with_capacity(WAL_HEADER_SIZE + n * frame_size);
2701 wal.extend_from_slice(&header_buf);
2702
2703 let mut running = read_wal_header_checksum(&header_buf).expect("read hdr cksum");
2704 for i in 0..n {
2705 let pg = u32::try_from(i + 1).unwrap();
2706 let mut frame = vec![0u8; frame_size];
2707 frame[..4].copy_from_slice(&pg.to_be_bytes());
2708 frame[4..8].copy_from_slice(&pg.to_be_bytes()); write_wal_frame_salts(&mut frame[..WAL_FRAME_HEADER_SIZE], salts).expect("frame salts");
2710 for (off, byte) in frame[WAL_FRAME_HEADER_SIZE..].iter_mut().enumerate() {
2711 let r = u8::try_from(off % 251).unwrap();
2712 let s = u8::try_from(i % 251).unwrap();
2713 *byte = r ^ s;
2714 }
2715 running =
2716 write_wal_frame_checksum(&mut frame, PAGE_SIZE, running, false).expect("cksum");
2717 wal.extend_from_slice(&frame);
2718 }
2719 wal
2720 }
2721
2722 #[test]
2723 fn test_classify_clean_wal_valid() {
2724 let wal = build_valid_wal(10);
2725 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
2726 assert!(v.valid);
2727 assert_eq!(v.valid_frames, 10);
2728 assert_eq!(v.replayable_frames, 10);
2729 assert!(v.reason.is_none());
2730 assert!(v.header_valid);
2731 }
2732
2733 #[test]
2734 fn test_classify_header_corruption() {
2735 let mut wal = build_valid_wal(5);
2736 wal[0] ^= 0xFF;
2738 let v = validate_wal_chain(&wal, PAGE_SIZE, false);
2739 if let Ok(val) = v {
2741 assert!(!val.header_valid);
2742 assert_eq!(
2743 val.reason,
2744 Some(WalChainInvalidReason::HeaderChecksumMismatch)
2745 );
2746 } }
2748
2749 #[test]
2750 fn test_classify_single_bit_flip_in_frame_data() {
2751 let mut wal = build_valid_wal(5);
2752 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
2753 let offset = WAL_HEADER_SIZE + 2 * frame_size + WAL_FRAME_HEADER_SIZE + 100;
2755 wal[offset] ^= 0x01;
2756 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
2757 assert!(!v.valid);
2758 assert_eq!(v.valid_frames, 2, "first 2 frames should survive");
2759 assert_eq!(v.first_invalid_frame, Some(2));
2760 assert_eq!(v.reason, Some(WalChainInvalidReason::FrameChecksumMismatch));
2761 }
2762
2763 #[test]
2764 fn test_classify_torn_write_mid_frame() {
2765 let wal = build_valid_wal(5);
2766 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
2767 let cut = WAL_HEADER_SIZE + 3 * frame_size + frame_size / 2;
2769 let torn = &wal[..cut];
2770 let v = validate_wal_chain(torn, PAGE_SIZE, false).expect("validate");
2771 assert_eq!(
2772 v.valid_frames, 3,
2773 "only 3 complete frames before truncation"
2774 );
2775 assert_eq!(v.reason, Some(WalChainInvalidReason::TruncatedFrame));
2776 }
2777
2778 #[test]
2779 fn test_classify_torn_write_in_header() {
2780 let wal = build_valid_wal(3);
2781 let torn = &wal[..16];
2783 let v = validate_wal_chain(torn, PAGE_SIZE, false);
2784 assert!(v.is_err() || !v.unwrap().header_valid);
2786 }
2787
2788 #[test]
2789 fn test_classify_salt_mismatch_in_frame() {
2790 let mut wal = build_valid_wal(5);
2791 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
2792 let salt_offset = WAL_HEADER_SIZE + frame_size + 8;
2794 wal[salt_offset] ^= 0xFF;
2795 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
2796 assert!(v.valid_frames <= 1, "at most frame 0 should survive");
2798 }
2799
2800 #[test]
2801 fn test_classify_zero_fill_corruption() {
2802 let mut wal = build_valid_wal(5);
2803 let start = WAL_HEADER_SIZE + WAL_FRAME_HEADER_SIZE;
2805 for byte in &mut wal[start..start + PAGE_SIZE] {
2806 *byte = 0;
2807 }
2808 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
2809 assert_eq!(v.valid_frames, 0, "frame 0 corrupted so 0 valid frames");
2810 assert_eq!(v.reason, Some(WalChainInvalidReason::FrameChecksumMismatch));
2811 }
2812
2813 #[test]
2814 fn test_classify_corruption_at_first_frame() {
2815 let mut wal = build_valid_wal(3);
2816 let offset = WAL_HEADER_SIZE + WAL_FRAME_HEADER_SIZE;
2818 wal[offset] ^= 0xAA;
2819 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
2820 assert_eq!(v.valid_frames, 0);
2821 assert_eq!(v.first_invalid_frame, Some(0));
2822 }
2823
2824 #[test]
2825 fn test_classify_corruption_at_last_frame() {
2826 let mut wal = build_valid_wal(5);
2827 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
2828 let offset = WAL_HEADER_SIZE + 4 * frame_size + WAL_FRAME_HEADER_SIZE + 50;
2830 wal[offset] ^= 0xBB;
2831 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
2832 assert_eq!(v.valid_frames, 4, "first 4 should survive");
2833 assert_eq!(v.first_invalid_frame, Some(4));
2834 }
2835
2836 #[test]
2837 fn test_detect_torn_write_true_on_truncation() {
2838 let wal = build_valid_wal(5);
2839 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
2840 let cut = WAL_HEADER_SIZE + 2 * frame_size + 10;
2841 let torn = &wal[..cut];
2842 assert!(detect_torn_write_in_wal(torn, PAGE_SIZE, false).expect("detect"));
2843 }
2844
2845 #[test]
2846 fn test_detect_torn_write_false_on_clean() {
2847 let wal = build_valid_wal(5);
2848 assert!(!detect_torn_write_in_wal(&wal, PAGE_SIZE, false).expect("detect"));
2849 }
2850
2851 #[test]
2852 fn test_detect_torn_write_true_on_bit_flip() {
2853 let mut wal = build_valid_wal(3);
2854 let offset = WAL_HEADER_SIZE + WAL_FRAME_HEADER_SIZE + 200;
2855 wal[offset] ^= 0x01;
2856 assert!(detect_torn_write_in_wal(&wal, PAGE_SIZE, false).expect("detect"));
2857 }
2858
2859 #[test]
2862 fn test_repair_decision_exact_boundary_symbols() {
2863 let action = recovery_action_for_checksum_failure(
2865 ChecksumFailureKind::WalFrameChecksumMismatch,
2866 Some(6),
2867 Some(6),
2868 );
2869 assert_eq!(action, RecoveryAction::AttemptWalFecRepair);
2870 }
2871
2872 #[test]
2873 fn test_repair_decision_one_short() {
2874 let action = recovery_action_for_checksum_failure(
2876 ChecksumFailureKind::WalFrameChecksumMismatch,
2877 Some(5),
2878 Some(6),
2879 );
2880 assert_eq!(action, RecoveryAction::TruncateWalAtFirstInvalidFrame);
2881 }
2882
2883 #[test]
2884 fn test_repair_decision_no_symbol_info() {
2885 let action = recovery_action_for_checksum_failure(
2887 ChecksumFailureKind::WalFrameChecksumMismatch,
2888 None,
2889 None,
2890 );
2891 assert_eq!(action, RecoveryAction::TruncateWalAtFirstInvalidFrame);
2892 }
2893
2894 #[test]
2895 fn test_repair_decision_partial_symbol_info() {
2896 let action = recovery_action_for_checksum_failure(
2898 ChecksumFailureKind::WalFrameChecksumMismatch,
2899 Some(10),
2900 None,
2901 );
2902 assert_eq!(action, RecoveryAction::TruncateWalAtFirstInvalidFrame);
2903 }
2904
2905 #[test]
2906 fn test_repair_decision_db_corruption_always_report() {
2907 let action = recovery_action_for_checksum_failure(
2908 ChecksumFailureKind::DbFileCorruption,
2909 Some(100),
2910 Some(1),
2911 );
2912 assert_eq!(action, RecoveryAction::ReportPersistentCorruption);
2913 }
2914
2915 #[test]
2916 fn test_attempt_fec_repair_insufficient_symbols() {
2917 let payload = sample_page(1);
2918 let hash = wal_fec_source_hash_xxh3_128(&payload);
2919 let result = attempt_wal_fec_repair(&payload, hash, 3, 6);
2920 assert_eq!(result, WalFecRepairOutcome::InsufficientSymbols);
2921 }
2922
2923 #[test]
2924 fn test_attempt_fec_repair_correct_hash() {
2925 let payload = sample_page(42);
2926 let hash = wal_fec_source_hash_xxh3_128(&payload);
2927 let result = attempt_wal_fec_repair(&payload, hash, 8, 6);
2928 assert_eq!(result, WalFecRepairOutcome::Repaired);
2929 }
2930
2931 #[test]
2932 fn test_attempt_fec_repair_wrong_hash() {
2933 let payload = sample_page(42);
2934 let wrong_hash = wal_fec_source_hash_xxh3_128(&sample_page(99));
2935 let result = attempt_wal_fec_repair(&payload, wrong_hash, 8, 6);
2936 assert_eq!(result, WalFecRepairOutcome::SourceHashMismatch);
2937 }
2938
2939 #[test]
2940 fn test_recover_decision_no_payload_truncates() {
2941 let decision = recover_wal_frame_checksum_mismatch(None, None, 10, 6);
2942 assert_eq!(decision, WalRecoveryDecision::Truncated);
2943 }
2944
2945 #[test]
2946 fn test_recover_decision_payload_but_no_hash_truncates() {
2947 let payload = sample_page(1);
2948 let decision = recover_wal_frame_checksum_mismatch(Some(&payload), None, 10, 6);
2949 assert_eq!(decision, WalRecoveryDecision::Truncated);
2950 }
2951
2952 #[test]
2953 fn test_recover_decision_full_repair_path() {
2954 let payload = sample_page(7);
2955 let hash = wal_fec_source_hash_xxh3_128(&payload);
2956 let decision = recover_wal_frame_checksum_mismatch(Some(&payload), Some(hash), 8, 6);
2957 assert_eq!(decision, WalRecoveryDecision::Repaired);
2958 }
2959
2960 #[test]
2961 fn test_all_failure_kinds_have_deterministic_action() {
2962 let kinds = [
2964 ChecksumFailureKind::WalFrameChecksumMismatch,
2965 ChecksumFailureKind::Xxh3PageChecksumMismatch,
2966 ChecksumFailureKind::Crc32cSymbolMismatch,
2967 ChecksumFailureKind::DbFileCorruption,
2968 ];
2969 for kind in kinds {
2970 let action = recovery_action_for_checksum_failure(kind, Some(10), Some(5));
2971 assert!(matches!(
2973 action,
2974 RecoveryAction::AttemptWalFecRepair
2975 | RecoveryAction::TruncateWalAtFirstInvalidFrame
2976 | RecoveryAction::EvictCacheAndRetryFromWal
2977 | RecoveryAction::ExcludeCorruptedSymbolAndContinue
2978 | RecoveryAction::ReportPersistentCorruption
2979 ));
2980 }
2981 }
2982
2983 #[test]
2984 fn test_multi_corruption_sites_first_wins() {
2985 let mut wal = build_valid_wal(10);
2987 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
2988 let off3 = WAL_HEADER_SIZE + 2 * frame_size + WAL_FRAME_HEADER_SIZE + 10;
2990 let off7 = WAL_HEADER_SIZE + 6 * frame_size + WAL_FRAME_HEADER_SIZE + 10;
2991 wal[off3] ^= 0xCC;
2992 wal[off7] ^= 0xDD;
2993 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
2994 assert_eq!(v.valid_frames, 2, "stops at first corruption (frame 3)");
2995 assert_eq!(v.first_invalid_frame, Some(2));
2996 }
2997
2998 #[test]
2999 fn test_crash_model_contract_flags_exhaustive() {
3000 let contract = crash_model_contract();
3001 assert!(contract.crash_at_any_point());
3002 assert!(contract.fsync_is_durability_barrier());
3003 assert!(contract.writes_reorder_without_fsync());
3004 assert!(contract.bitrot_exists());
3005 assert!(contract.metadata_may_require_directory_fsync());
3006 }
3007
3008 #[test]
3009 fn test_replayable_frames_stop_at_last_commit() {
3010 let salts = WalSalts {
3014 salt1: 0x1111_2222,
3015 salt2: 0x3333_4444,
3016 };
3017 let mut hdr = [0u8; WAL_HEADER_SIZE];
3018 hdr[..4].copy_from_slice(&WAL_MAGIC_LE.to_be_bytes());
3019 hdr[4..8].copy_from_slice(&WAL_FORMAT_VERSION.to_be_bytes());
3020 hdr[8..12].copy_from_slice(&u32::try_from(PAGE_SIZE).unwrap().to_be_bytes());
3021 write_wal_header_salts(&mut hdr, salts).expect("salts");
3022 write_wal_header_checksum(&mut hdr, false).expect("hdr cksum");
3023
3024 let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE;
3025 let mut wal = Vec::with_capacity(WAL_HEADER_SIZE + 5 * frame_size);
3026 wal.extend_from_slice(&hdr);
3027 let mut running = read_wal_header_checksum(&hdr).expect("seed");
3028
3029 for i in 0..5u32 {
3030 let mut frame = vec![0u8; frame_size];
3031 frame[..4].copy_from_slice(&(i + 1).to_be_bytes());
3032 let db_size = if i < 3 { i + 1 } else { 0 };
3034 frame[4..8].copy_from_slice(&db_size.to_be_bytes());
3035 write_wal_frame_salts(&mut frame[..WAL_FRAME_HEADER_SIZE], salts).expect("salts");
3036 for (off, byte) in frame[WAL_FRAME_HEADER_SIZE..].iter_mut().enumerate() {
3037 *byte = u8::try_from((off + usize::try_from(i).unwrap()) % 251).unwrap();
3038 }
3039 running =
3040 write_wal_frame_checksum(&mut frame, PAGE_SIZE, running, false).expect("cksum");
3041 wal.extend_from_slice(&frame);
3042 }
3043
3044 let v = validate_wal_chain(&wal, PAGE_SIZE, false).expect("validate");
3045 assert!(v.valid, "all 5 pass checksum");
3046 assert_eq!(v.valid_frames, 5);
3047 assert_eq!(v.replayable_frames, 3);
3049 assert_eq!(v.last_commit_frame, Some(2));
3050 }
3051
3052 #[test]
3053 fn test_tier_for_algorithm_known_and_unknown() {
3054 assert_eq!(tier_for_algorithm("xxh3_128"), Some(HashTier::Integrity));
3055 assert_eq!(tier_for_algorithm("xxh3"), Some(HashTier::Integrity));
3056 assert_eq!(
3057 tier_for_algorithm("blake3_128"),
3058 Some(HashTier::ContentAddressing)
3059 );
3060 assert_eq!(
3061 tier_for_algorithm("blake3"),
3062 Some(HashTier::ContentAddressing)
3063 );
3064 assert_eq!(tier_for_algorithm("crc32c"), Some(HashTier::Protocol));
3065 assert_eq!(tier_for_algorithm(" XXH3 "), Some(HashTier::Integrity));
3066 assert_eq!(
3067 tier_for_algorithm("BLAKE3_128"),
3068 Some(HashTier::ContentAddressing)
3069 );
3070 assert_eq!(tier_for_algorithm("sha256"), None);
3071 assert_eq!(tier_for_algorithm(""), None);
3072 }
3073
3074 #[test]
3075 fn test_checksum_transform_identity_is_noop() {
3076 let id = WalChecksumTransform::identity();
3077 for seed in [
3078 SqliteWalChecksum { s1: 0, s2: 0 },
3079 SqliteWalChecksum {
3080 s1: 0xDEAD_BEEF,
3081 s2: 0xCAFE_BABE,
3082 },
3083 SqliteWalChecksum {
3084 s1: u32::MAX,
3085 s2: u32::MAX,
3086 },
3087 ] {
3088 assert_eq!(id.apply(seed), seed, "identity must leave seed unchanged");
3089 }
3090 }
3091
3092 #[test]
3093 fn test_checksum_transform_then_identity_laws() {
3094 let data = vec![0x42_u8; 64];
3095 let t = WalChecksumTransform::from_aligned_bytes(&data, false).expect("transform");
3096 let id = WalChecksumTransform::identity();
3097 let seed = SqliteWalChecksum {
3098 s1: 0x1111_2222,
3099 s2: 0x3333_4444,
3100 };
3101 assert_eq!(id.then(t).apply(seed), t.apply(seed), "id.then(t) == t");
3102 assert_eq!(t.then(id).apply(seed), t.apply(seed), "t.then(id) == t");
3103 }
3104
3105 #[test]
3106 fn test_xxh3_checksum128_to_le_bytes_roundtrip() {
3107 let data = b"deterministic test payload";
3108 let digest = Xxh3Checksum128::compute(data);
3109 let le = digest.to_le_bytes();
3110 let reconstructed = read_xxh3_from_bytes(&le);
3111 assert_eq!(digest, reconstructed);
3112 }
3113
3114 #[test]
3115 fn test_crc32c_and_content_address_determinism() {
3116 let data = b"hello world";
3117 let c1 = crc32c_checksum(data);
3118 let c2 = crc32c_checksum(data);
3119 assert_eq!(c1, c2, "crc32c must be deterministic");
3120 assert_ne!(crc32c_checksum(data), crc32c_checksum(b"hello worlD"));
3121
3122 let h1 = content_address_hash_128(data);
3123 let h2 = content_address_hash_128(data);
3124 assert_eq!(h1, h2, "content_address_hash must be deterministic");
3125 assert_ne!(
3126 content_address_hash_128(data),
3127 content_address_hash_128(b"different")
3128 );
3129
3130 let i1 = integrity_hash_xxh3_128(data);
3131 let i2 = integrity_hash_xxh3_128(data);
3132 assert_eq!(i1, i2, "integrity_hash must be deterministic");
3133 }
3134
3135 #[test]
3136 fn integrity_check_report_ok_and_sqlite_messages() {
3137 let report = IntegrityCheckReport::ok(10);
3138 assert!(report.is_ok());
3139 assert_eq!(report.pages_checked, 10);
3140 assert_eq!(report.sqlite_messages(), vec!["ok"]);
3141 }
3142
3143 #[test]
3144 fn integrity_check_report_with_issues() {
3145 let mut report = IntegrityCheckReport::ok(5);
3146 report.push(IntegrityCheckLevel::Page, Some(3), "bad page");
3147 report.push(IntegrityCheckLevel::Schema, None, "schema err");
3148 assert!(!report.is_ok());
3149 assert_eq!(report.issues.len(), 2);
3150 let msgs = report.sqlite_messages();
3151 assert_eq!(msgs, vec!["bad page", "schema err"]);
3152 assert_eq!(report.issues[0].level, IntegrityCheckLevel::Page);
3153 assert_eq!(report.issues[1].page_number, None);
3154 }
3155
3156 #[test]
3157 fn checksum_failure_kind_all_variants_copy_eq() {
3158 let variants = [
3159 ChecksumFailureKind::WalFrameChecksumMismatch,
3160 ChecksumFailureKind::Xxh3PageChecksumMismatch,
3161 ChecksumFailureKind::Crc32cSymbolMismatch,
3162 ChecksumFailureKind::DbFileCorruption,
3163 ];
3164 for (i, v) in variants.iter().enumerate() {
3165 let copied = *v;
3166 assert_eq!(copied, *v);
3167 for (j, w) in variants.iter().enumerate() {
3168 assert_eq!(i == j, v == w);
3169 }
3170 }
3171 let dbg = format!("{:?}", ChecksumFailureKind::Crc32cSymbolMismatch);
3172 assert!(dbg.contains("Crc32cSymbolMismatch"));
3173 }
3174
3175 #[test]
3176 fn integrity_check_level_all_variants_copy_eq() {
3177 let variants = [
3178 IntegrityCheckLevel::Page,
3179 IntegrityCheckLevel::BtreeStructural,
3180 IntegrityCheckLevel::RecordFormat,
3181 IntegrityCheckLevel::CrossReference,
3182 IntegrityCheckLevel::Schema,
3183 ];
3184 for (i, v) in variants.iter().enumerate() {
3185 let copied = *v;
3186 assert_eq!(copied, *v);
3187 for (j, w) in variants.iter().enumerate() {
3188 assert_eq!(i == j, v == w);
3189 }
3190 }
3191 let dbg = format!("{:?}", IntegrityCheckLevel::BtreeStructural);
3192 assert!(dbg.contains("BtreeStructural"));
3193 }
3194
3195 #[test]
3196 fn recovery_action_all_variants_copy_eq_debug() {
3197 let variants = [
3198 RecoveryAction::AttemptWalFecRepair,
3199 RecoveryAction::TruncateWalAtFirstInvalidFrame,
3200 RecoveryAction::EvictCacheAndRetryFromWal,
3201 RecoveryAction::ExcludeCorruptedSymbolAndContinue,
3202 RecoveryAction::ReportPersistentCorruption,
3203 ];
3204 for (i, v) in variants.iter().enumerate() {
3205 let copied = *v;
3206 assert_eq!(copied, *v);
3207 for (j, w) in variants.iter().enumerate() {
3208 assert_eq!(i == j, v == w);
3209 }
3210 }
3211 let dbg = format!("{:?}", RecoveryAction::AttemptWalFecRepair);
3212 assert!(dbg.contains("AttemptWalFecRepair"));
3213 }
3214
3215 #[test]
3216 fn wal_recovery_decision_and_fec_repair_outcome_copy_eq() {
3217 let d1 = WalRecoveryDecision::Repaired;
3218 let d2 = WalRecoveryDecision::Truncated;
3219 assert_eq!(d1, d1);
3220 assert_ne!(d1, d2);
3221 let copied = d1;
3222 assert_eq!(copied, WalRecoveryDecision::Repaired);
3223 assert!(format!("{d2:?}").contains("Truncated"));
3224
3225 let outcomes = [
3226 WalFecRepairOutcome::Repaired,
3227 WalFecRepairOutcome::InsufficientSymbols,
3228 WalFecRepairOutcome::SourceHashMismatch,
3229 ];
3230 for (i, o) in outcomes.iter().enumerate() {
3231 let copied = *o;
3232 assert_eq!(copied, *o);
3233 for (j, p) in outcomes.iter().enumerate() {
3234 assert_eq!(i == j, o == p);
3235 }
3236 }
3237 assert!(
3238 format!("{:?}", WalFecRepairOutcome::SourceHashMismatch).contains("SourceHashMismatch")
3239 );
3240 }
3241
3242 #[test]
3243 fn crash_model_contract_default_all_flags_set_and_accessors() {
3244 let c = CrashModelContract::default();
3245 assert!(c.crash_at_any_point());
3246 assert!(c.fsync_is_durability_barrier());
3247 assert!(c.writes_reorder_without_fsync());
3248 assert!(c.bitrot_exists());
3249 assert!(c.metadata_may_require_directory_fsync());
3250 assert_eq!(c, crash_model_contract());
3251
3252 let empty = CrashModelContract { flags: 0 };
3253 assert!(!empty.crash_at_any_point());
3254 assert!(!empty.fsync_is_durability_barrier());
3255 assert!(!empty.writes_reorder_without_fsync());
3256 assert!(!empty.bitrot_exists());
3257 assert!(!empty.metadata_may_require_directory_fsync());
3258 assert_ne!(c, empty);
3259 assert!(format!("{c:?}").contains("CrashModelContract"));
3260 }
3261
3262 #[test]
3263 fn sector_sizes_and_btree_page_types_constants() {
3264 assert!(supports_torn_write_sector_size(512));
3265 assert!(supports_torn_write_sector_size(1024));
3266 assert!(supports_torn_write_sector_size(4096));
3267 assert!(!supports_torn_write_sector_size(2048));
3268 assert!(!supports_torn_write_sector_size(0));
3269
3270 assert!(is_valid_btree_page_type(0x02));
3271 assert!(is_valid_btree_page_type(0x05));
3272 assert!(is_valid_btree_page_type(0x0A));
3273 assert!(is_valid_btree_page_type(0x0D));
3274 assert!(!is_valid_btree_page_type(0x00));
3275 assert!(!is_valid_btree_page_type(0xFF));
3276 }
3277}