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