1use crate::vfs::VfsFile;
8use std::io::SeekFrom;
9use std::path::{Path, PathBuf};
10
11#[cfg(not(target_arch = "wasm32"))]
12mod native;
13#[cfg(not(target_arch = "wasm32"))]
14pub use native::EmbeddedRdbWriterLock;
15
16mod portable;
17pub use portable::EmbeddedRdbFile;
18
19#[cfg(not(target_arch = "wasm32"))]
20mod group_commit;
21#[cfg(not(target_arch = "wasm32"))]
22pub use group_commit::{
23 EmbeddedGroupCommitOptions, EmbeddedGroupCommitSession, PendingEmbeddedAppend,
24};
25
26pub type RdbFileResult<T> = Result<T, RdbFileError>;
27
28pub const DEFAULT_FORMAT_VERSION: u32 = 1;
29
30#[derive(Debug)]
31pub enum RdbFileError {
32 ReadLimitExceeded {
33 required_bytes: usize,
34 limit_bytes: usize,
35 },
36 InvalidOperation(String),
37 Io(std::io::Error),
38 ZoneUnrecoverable {
42 zone: &'static str,
43 path: PathBuf,
44 },
45}
46
47impl std::fmt::Display for RdbFileError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::ReadLimitExceeded {
51 required_bytes,
52 limit_bytes,
53 } => write!(
54 f,
55 "native decode needs {required_bytes} bytes over read memory limit {limit_bytes}"
56 ),
57 Self::InvalidOperation(msg) => write!(f, "INVALID_OPERATION: {msg}"),
58 Self::Io(err) => write!(f, "io error: {err}"),
59 Self::ZoneUnrecoverable { zone, path } => write!(
60 f,
61 "{zone} zone of {} failed validation, so the store will not be opened \
62 (opening it could only return data the zone can no longer vouch for). \
63 Run the `SCRUB` statement to classify the fault and `red salvage` to \
64 extract every entity the damage did not touch; `red salvage` never writes \
65 into the damaged file (ADR 0074 §2/§4).",
66 path.display()
67 ),
68 }
69 }
70}
71
72impl std::error::Error for RdbFileError {}
73
74impl From<std::io::Error> for RdbFileError {
75 fn from(err: std::io::Error) -> Self {
76 Self::Io(err)
77 }
78}
79
80fn crc32(data: &[u8]) -> u32 {
81 let mut hasher = crc32fast::Hasher::new();
82 hasher.update(data);
83 hasher.finalize()
84}
85
86pub const EMBEDDED_RDB_SUPERBLOCK_SIZE: u64 = 4096;
87pub const EMBEDDED_RDB_SUPERBLOCK_0_OFFSET: u64 = 0;
88pub const EMBEDDED_RDB_SUPERBLOCK_1_OFFSET: u64 = EMBEDDED_RDB_SUPERBLOCK_SIZE;
89
90pub const EMBEDDED_RDB_MANIFEST_SLOT_SIZE: u64 = 4096;
99pub const EMBEDDED_RDB_MANIFEST_0_OFFSET: u64 = EMBEDDED_RDB_SUPERBLOCK_SIZE * 2;
100pub const EMBEDDED_RDB_MANIFEST_1_OFFSET: u64 =
101 EMBEDDED_RDB_MANIFEST_0_OFFSET + EMBEDDED_RDB_MANIFEST_SLOT_SIZE;
102pub const EMBEDDED_RDB_MANIFEST_ZONE_END: u64 =
103 EMBEDDED_RDB_MANIFEST_1_OFFSET + EMBEDDED_RDB_MANIFEST_SLOT_SIZE;
104
105const SUPERBLOCK_MAGIC: &[u8; 8] = b"RDBSBLK1";
106const MANIFEST_MAGIC: &[u8; 8] = b"RDBMNFS1";
107const SUPERBLOCK_VERSION: u32 = 2;
108const MANIFEST_VERSION: u32 = 2;
109const LEGACY_SUPERBLOCK_VERSION: u32 = 1;
110const LEGACY_MANIFEST_VERSION: u32 = 1;
111const CHECKSUM_LEN: usize = 4;
112const MANIFEST_REGION_BYTES: u64 = EMBEDDED_RDB_MANIFEST_SLOT_SIZE;
113const WAL_REGION_BYTES: u64 = 64 * 1024;
114const SNAPSHOT_ALIGNMENT: u64 = 4096;
115const SNAPSHOT_MAGIC: &[u8; 4] = b"RDST";
116const WAL_FRAME_MAGIC: &[u8; 8] = b"RDBEWAL1";
117const WAL_FRAME_VERSION: u16 = 2;
118const WAL_FRAME_HEADER_BYTES: usize = 8 + 2 + 2 + 8 + 4 + 4 + 4 + 4;
119const LEGACY_WAL_FRAME_HEADER_BYTES: usize = 8 + 4 + 4;
120#[cfg(not(target_arch = "wasm32"))]
121const CRASH_INJECT_ENV: &str = "REDDB_EMBEDDED_RDB_CRASH_AT";
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct EmbeddedRdbManifest {
125 pub version: u32,
126 pub wal_region_offset: u64,
127 pub wal_region_bytes: u64,
128 pub wal_recovery_boundary: u64,
129 pub wal_live_bytes: u64,
130 pub snapshot_offset: u64,
131 pub snapshot_bytes: u64,
132 pub snapshot_checksum: u32,
133 pub created_at_unix_ms: u128,
134 pub checksum: u32,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct EmbeddedRdbSuperblock {
139 pub copy_index: u8,
140 pub generation: u64,
141 pub format_version: u32,
142 pub manifest_offset: u64,
143 pub manifest_len: u64,
144 pub manifest_checksum: u32,
145 pub wal_region_offset: u64,
146 pub wal_region_bytes: u64,
147 pub wal_recovery_boundary: u64,
148 pub wal_live_bytes: u64,
149 pub snapshot_offset: u64,
150 pub snapshot_bytes: u64,
151 pub snapshot_checksum: u32,
152 pub checksum: u32,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct EmbeddedRdbOpen {
157 pub path: PathBuf,
158 pub selected_superblock: EmbeddedRdbSuperblock,
159 pub manifest: EmbeddedRdbManifest,
160}
161
162#[derive(Debug, Default)]
163struct WalScan {
164 payloads: Vec<Vec<u8>>,
165 next_sequence: u64,
166 previous_frame_crc: u32,
167 valid_bytes: u64,
168}
169
170pub struct EmbeddedRdbArtifact;
171
172impl EmbeddedRdbArtifact {
173 fn open_inner_with_file(
187 file: &mut impl VfsFile,
188 path: &Path,
189 validate_snapshot_refs: bool,
190 ) -> RdbFileResult<EmbeddedRdbOpen> {
191 Self::open_inner_selected(file, path, validate_snapshot_refs).map(|(open, _)| open)
192 }
193
194 fn open_inner_with_snapshot(
195 file: &mut impl VfsFile,
196 path: &Path,
197 ) -> RdbFileResult<(EmbeddedRdbOpen, Option<Vec<u8>>)> {
198 Self::open_inner_selected(file, path, true)
199 }
200
201 fn open_inner_selected(
202 file: &mut impl VfsFile,
203 path: &Path,
204 validate_snapshot_refs: bool,
205 ) -> RdbFileResult<(EmbeddedRdbOpen, Option<Vec<u8>>)> {
206 let mut superblocks: Vec<EmbeddedRdbSuperblock> = [
209 read_superblock_copy(file, 0)?,
210 read_superblock_copy(file, 1)?,
211 ]
212 .into_iter()
213 .flatten()
214 .collect();
215 superblocks.sort_by_key(|superblock| std::cmp::Reverse(superblock.generation));
216
217 if superblocks.is_empty() {
218 return Err(RdbFileError::ZoneUnrecoverable {
219 zone: "superblock",
220 path: path.to_path_buf(),
221 });
222 }
223
224 for selected_superblock in superblocks {
225 let mut manifest = read_manifest(file, selected_superblock).map_err(|_| {
231 RdbFileError::ZoneUnrecoverable {
232 zone: "manifest",
233 path: path.to_path_buf(),
234 }
235 })?;
236 manifest.wal_recovery_boundary = selected_superblock.wal_recovery_boundary;
237 manifest.wal_live_bytes = selected_superblock.wal_live_bytes;
238 let snapshot = if validate_snapshot_refs {
239 let Some(snapshot) = read_valid_snapshot_reference(file, &manifest)? else {
240 continue;
241 };
242 (manifest.snapshot_bytes != 0).then_some(snapshot)
243 } else {
244 None
245 };
246 return Ok((
247 EmbeddedRdbOpen {
248 path: path.to_path_buf(),
249 selected_superblock,
250 manifest,
251 },
252 snapshot,
253 ));
254 }
255
256 Err(RdbFileError::ZoneUnrecoverable {
257 zone: "snapshot",
258 path: path.to_path_buf(),
259 })
260 }
261
262 pub fn wal_payloads_encoded_len(payloads: &[Vec<u8>]) -> RdbFileResult<u64> {
263 let mut len = 0u64;
264 for payload in payloads {
265 let payload_len = u32::try_from(payload.len()).map_err(|_| {
266 RdbFileError::InvalidOperation("embedded wal payload too large".into())
267 })?;
268 let frame_len = WAL_FRAME_HEADER_BYTES as u64 + payload_len as u64;
269 len = len.checked_add(frame_len).ok_or_else(|| {
270 RdbFileError::InvalidOperation("embedded wal encoded length overflow".into())
271 })?;
272 }
273 Ok(len)
274 }
275}
276
277fn read_superblock_copy(
278 file: &mut impl VfsFile,
279 copy_index: u8,
280) -> RdbFileResult<Option<EmbeddedRdbSuperblock>> {
281 let offset = superblock_offset(copy_index)?;
282 let mut bytes = vec![0u8; EMBEDDED_RDB_SUPERBLOCK_SIZE as usize];
283 file.seek(SeekFrom::Start(offset))?;
284 file.read_exact(&mut bytes)?;
285 Ok(decode_superblock(copy_index, &bytes).ok())
286}
287
288fn inactive_manifest_offset(superblock: EmbeddedRdbSuperblock) -> RdbFileResult<u64> {
295 match superblock.manifest_offset {
296 EMBEDDED_RDB_MANIFEST_0_OFFSET => Ok(EMBEDDED_RDB_MANIFEST_1_OFFSET),
297 EMBEDDED_RDB_MANIFEST_1_OFFSET => Ok(EMBEDDED_RDB_MANIFEST_0_OFFSET),
298 other => Err(RdbFileError::InvalidOperation(format!(
299 "superblock names a manifest offset outside the zone: {other}"
300 ))),
301 }
302}
303
304fn read_manifest(
305 file: &mut impl VfsFile,
306 superblock: EmbeddedRdbSuperblock,
307) -> RdbFileResult<EmbeddedRdbManifest> {
308 if superblock.manifest_len < CHECKSUM_LEN as u64
309 || superblock.manifest_len > MANIFEST_REGION_BYTES
310 {
311 return Err(RdbFileError::InvalidOperation(format!(
312 "invalid embedded manifest length {}",
313 superblock.manifest_len
314 )));
315 }
316 if !matches!(
319 superblock.manifest_offset,
320 EMBEDDED_RDB_MANIFEST_0_OFFSET | EMBEDDED_RDB_MANIFEST_1_OFFSET
321 ) {
322 return Err(RdbFileError::InvalidOperation(format!(
323 "superblock names a manifest offset outside the zone: {}",
324 superblock.manifest_offset
325 )));
326 }
327
328 let mut bytes = vec![0u8; superblock.manifest_len as usize];
329 file.seek(SeekFrom::Start(superblock.manifest_offset))?;
330 file.read_exact(&mut bytes)?;
331 let checksum = trailer_checksum(&bytes);
332 if checksum != superblock.manifest_checksum {
333 return Err(RdbFileError::InvalidOperation(format!(
334 "embedded manifest checksum mismatch: stored {:#010x}, computed {:#010x}",
335 superblock.manifest_checksum, checksum
336 )));
337 }
338 decode_manifest(&bytes)
339}
340
341fn read_valid_snapshot_reference(
342 file: &mut impl VfsFile,
343 manifest: &EmbeddedRdbManifest,
344) -> RdbFileResult<Option<Vec<u8>>> {
345 if manifest.snapshot_bytes == 0 {
346 return Ok(Some(Vec::new()));
347 }
348 let snapshot_end = manifest
349 .snapshot_offset
350 .checked_add(manifest.snapshot_bytes)
351 .ok_or_else(|| RdbFileError::InvalidOperation("embedded snapshot end overflow".into()))?;
352 if snapshot_end > file.file_len()? {
353 return Ok(None);
354 }
355
356 let mut bytes = vec![0u8; manifest.snapshot_bytes as usize];
357 file.seek(SeekFrom::Start(manifest.snapshot_offset))?;
358 if file.read_exact(&mut bytes).is_err() {
359 return Ok(None);
360 }
361 if crc32(&bytes) != manifest.snapshot_checksum {
362 return Ok(None);
363 }
364 if bytes.len() >= SNAPSHOT_MAGIC.len() && &bytes[..SNAPSHOT_MAGIC.len()] != SNAPSHOT_MAGIC {
365 return Ok(None);
366 }
367 Ok(Some(bytes))
368}
369
370fn published_snapshot_matches(
373 file: &mut impl VfsFile,
374 open: &EmbeddedRdbOpen,
375 snapshot: &[u8],
376) -> RdbFileResult<bool> {
377 if snapshot.is_empty() {
378 return Ok(true);
379 }
380 let mut published = vec![0u8; snapshot.len()];
381 file.seek(SeekFrom::Start(open.manifest.snapshot_offset))?;
382 file.read_exact(&mut published)?;
383 Ok(published == snapshot)
384}
385
386fn grow_wal_region_bytes(current: u64, min_required: u64) -> RdbFileResult<u64> {
387 let mut next = current.max(WAL_REGION_BYTES);
388 while next < min_required {
389 next = next.checked_mul(2).ok_or_else(|| {
390 RdbFileError::InvalidOperation("embedded wal region size overflow".into())
391 })?;
392 }
393 Ok(next)
394}
395
396fn next_snapshot_offset(
404 file: &mut impl VfsFile,
405 open: &EmbeddedRdbOpen,
406 wal_region_bytes: u64,
407 snapshot: &[u8],
408) -> RdbFileResult<u64> {
409 let base = open
410 .manifest
411 .wal_region_offset
412 .checked_add(wal_region_bytes)
413 .ok_or_else(|| {
414 RdbFileError::InvalidOperation("embedded snapshot offset overflow".into())
415 })?;
416 if open.manifest.snapshot_bytes == 0 && snapshot.is_empty() {
417 return Ok(base);
418 }
419
420 let other_copy = if open.selected_superblock.copy_index == 0 {
421 1
422 } else {
423 0
424 };
425 let mut live = vec![(open.manifest.snapshot_offset, open.manifest.snapshot_bytes)];
426 if let Some(other) = read_superblock_copy(file, other_copy)? {
427 live.push((other.snapshot_offset, other.snapshot_bytes));
428 }
429 let mut live_ranges = Vec::with_capacity(live.len());
430 for (offset, bytes) in live {
431 if bytes == 0 {
432 continue;
433 }
434 let end = offset.checked_add(bytes).ok_or_else(|| {
435 RdbFileError::InvalidOperation("embedded snapshot end overflow".into())
436 })?;
437 live_ranges.push((offset, end));
438 }
439
440 let len = snapshot.len() as u64;
441 let mut candidates = vec![align_up(base, SNAPSHOT_ALIGNMENT)?];
442 for &(_, end) in &live_ranges {
443 candidates.push(align_up(end.max(base), SNAPSHOT_ALIGNMENT)?);
444 }
445 candidates.sort_unstable();
446 for &candidate in &candidates {
447 let end = candidate.checked_add(len).ok_or_else(|| {
448 RdbFileError::InvalidOperation("embedded snapshot end overflow".into())
449 })?;
450 if live_ranges
451 .iter()
452 .all(|&(start, live_end)| end <= start || candidate >= live_end)
453 {
454 return Ok(candidate);
455 }
456 }
457 Ok(candidates[candidates.len() - 1])
459}
460
461fn align_up(value: u64, alignment: u64) -> RdbFileResult<u64> {
462 if alignment == 0 {
463 return Ok(value);
464 }
465 let remainder = value % alignment;
466 if remainder == 0 {
467 return Ok(value);
468 }
469 value
470 .checked_add(alignment - remainder)
471 .ok_or_else(|| RdbFileError::InvalidOperation("embedded alignment overflow".into()))
472}
473
474fn wal_region_start_checked(open: &EmbeddedRdbOpen) -> RdbFileResult<Option<u64>> {
478 let wal_start = open.manifest.wal_region_offset;
479 let wal_end = wal_start
480 .checked_add(open.manifest.wal_region_bytes)
481 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal end overflow".into()))?;
482 let append_boundary = open.manifest.wal_recovery_boundary;
483 if append_boundary < wal_start || append_boundary > wal_end {
484 return Err(RdbFileError::InvalidOperation(format!(
485 "invalid embedded wal boundary {append_boundary}"
486 )));
487 }
488 if open.manifest.wal_live_bytes > open.manifest.wal_region_bytes {
489 return Err(RdbFileError::InvalidOperation(format!(
490 "invalid embedded wal live bytes {}",
491 open.manifest.wal_live_bytes
492 )));
493 }
494 if open.manifest.wal_live_bytes == 0 {
495 return Ok(None);
496 }
497 Ok(Some(wal_start))
498}
499
500fn scan_wal_with_file(
504 file: &mut impl VfsFile,
505 open: &EmbeddedRdbOpen,
506 wal_start: u64,
507) -> RdbFileResult<WalScan> {
508 let file_len = file.file_len()?;
509 if file_len <= wal_start {
510 return Ok(WalScan {
511 next_sequence: 1,
512 ..WalScan::default()
513 });
514 }
515 let bytes = read_circular_wal_bytes(file, open, file_len)?;
516 Ok(scan_wal_bytes(&bytes))
517}
518
519fn read_circular_wal_bytes(
520 file: &mut impl VfsFile,
521 open: &EmbeddedRdbOpen,
522 file_len: u64,
523) -> RdbFileResult<Vec<u8>> {
524 let live_start = wal_live_start_relative(open)?;
525 let region_bytes = open.manifest.wal_region_bytes;
526 let live_bytes = open.manifest.wal_live_bytes;
527 let mut remaining = live_bytes.min(file_len.saturating_sub(open.manifest.wal_region_offset));
528 let mut relative = live_start;
529 let mut bytes = Vec::with_capacity(remaining as usize);
530 while remaining > 0 {
531 let contiguous = (region_bytes - relative).min(remaining);
532 let offset = open.manifest.wal_region_offset + relative;
533 file.seek(SeekFrom::Start(offset))?;
534 let mut chunk = vec![0u8; contiguous as usize];
535 file.read_exact(&mut chunk)?;
536 bytes.extend_from_slice(&chunk);
537 remaining -= contiguous;
538 relative = 0;
539 }
540 Ok(bytes)
541}
542
543fn write_circular_wal_bytes(
544 file: &mut impl VfsFile,
545 open: &EmbeddedRdbOpen,
546 bytes: &[u8],
547) -> RdbFileResult<()> {
548 let mut written = 0usize;
549 let mut relative = wal_append_relative(open)?;
550 while written < bytes.len() {
551 if relative == open.manifest.wal_region_bytes {
552 relative = 0;
553 }
554 let contiguous = (open.manifest.wal_region_bytes - relative) as usize;
555 let chunk_len = contiguous.min(bytes.len() - written);
556 write_at(
557 file,
558 open.manifest.wal_region_offset + relative,
559 &bytes[written..written + chunk_len],
560 )?;
561 written += chunk_len;
562 relative += chunk_len as u64;
563 }
564 Ok(())
565}
566
567fn wal_append_relative(open: &EmbeddedRdbOpen) -> RdbFileResult<u64> {
568 open.manifest
569 .wal_recovery_boundary
570 .checked_sub(open.manifest.wal_region_offset)
571 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal boundary underflow".into()))
572}
573
574fn wal_live_start_relative(open: &EmbeddedRdbOpen) -> RdbFileResult<u64> {
575 let append = wal_append_relative(open)?;
576 let region = open.manifest.wal_region_bytes;
577 if region == 0 {
578 return Err(RdbFileError::InvalidOperation(
579 "embedded wal region is empty".into(),
580 ));
581 }
582 Ok((append + region - (open.manifest.wal_live_bytes % region)) % region)
583}
584
585fn wal_boundary_after_live_bytes(open: &EmbeddedRdbOpen, live_bytes: u64) -> RdbFileResult<u64> {
586 if live_bytes > open.manifest.wal_region_bytes {
587 return Err(RdbFileError::InvalidOperation(format!(
588 "embedded wal live bytes {live_bytes} exceed region size {}",
589 open.manifest.wal_region_bytes
590 )));
591 }
592 let start = if open.manifest.wal_live_bytes == 0 {
593 wal_append_relative(open)?
594 } else {
595 wal_live_start_relative(open)?
596 };
597 let relative = (start + live_bytes) % open.manifest.wal_region_bytes;
598 open.manifest
599 .wal_region_offset
600 .checked_add(relative)
601 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal boundary overflow".into()))
602}
603
604fn scan_wal_bytes(bytes: &[u8]) -> WalScan {
605 let mut scan = WalScan {
606 next_sequence: 1,
607 ..WalScan::default()
608 };
609 let mut cursor = 0usize;
610 while cursor < bytes.len() {
611 let Some(frame) = decode_next_wal_frame(bytes, cursor, &scan) else {
612 break;
613 };
614 scan.payloads.push(frame.payload);
615 scan.next_sequence = scan.next_sequence.saturating_add(1);
616 scan.previous_frame_crc = frame.frame_crc;
617 cursor = frame.end;
618 scan.valid_bytes = cursor as u64;
619 }
620 scan
621}
622
623struct DecodedWalFrame {
624 payload: Vec<u8>,
625 frame_crc: u32,
626 end: usize,
627}
628
629fn decode_next_wal_frame(bytes: &[u8], start: usize, scan: &WalScan) -> Option<DecodedWalFrame> {
630 let remaining = bytes.len().checked_sub(start)?;
631 if remaining < WAL_FRAME_MAGIC.len() {
632 return None;
633 }
634 if &bytes[start..start + WAL_FRAME_MAGIC.len()] != WAL_FRAME_MAGIC {
635 return None;
636 }
637 if remaining < WAL_FRAME_MAGIC.len() + 2 {
638 return None;
639 }
640 let version_offset = start + WAL_FRAME_MAGIC.len();
641 let version = u16::from_le_bytes(bytes[version_offset..version_offset + 2].try_into().ok()?);
642 if version == WAL_FRAME_VERSION {
643 decode_v2_wal_frame(bytes, start, scan)
644 } else {
645 decode_legacy_wal_frame(bytes, start)
646 }
647}
648
649fn decode_v2_wal_frame(bytes: &[u8], start: usize, scan: &WalScan) -> Option<DecodedWalFrame> {
650 if bytes.len().checked_sub(start)? < WAL_FRAME_HEADER_BYTES {
651 return None;
652 }
653 let header_len_offset = start + 10;
654 let header_len = u16::from_le_bytes(
655 bytes[header_len_offset..header_len_offset + 2]
656 .try_into()
657 .ok()?,
658 ) as usize;
659 if header_len != WAL_FRAME_HEADER_BYTES {
660 return None;
661 }
662 let sequence_offset = start + 12;
663 let sequence = u64::from_le_bytes(
664 bytes[sequence_offset..sequence_offset + 8]
665 .try_into()
666 .ok()?,
667 );
668 if sequence != scan.next_sequence {
669 return None;
670 }
671 let payload_len_offset = start + 20;
672 let payload_len = u32::from_le_bytes(
673 bytes[payload_len_offset..payload_len_offset + 4]
674 .try_into()
675 .ok()?,
676 ) as usize;
677 let payload_crc_offset = start + 24;
678 let payload_crc = u32::from_le_bytes(
679 bytes[payload_crc_offset..payload_crc_offset + 4]
680 .try_into()
681 .ok()?,
682 );
683 let previous_frame_crc_offset = start + 28;
684 let previous_frame_crc = u32::from_le_bytes(
685 bytes[previous_frame_crc_offset..previous_frame_crc_offset + 4]
686 .try_into()
687 .ok()?,
688 );
689 if previous_frame_crc != scan.previous_frame_crc {
690 return None;
691 }
692 let header_crc_offset = start + 32;
693 let header_crc = u32::from_le_bytes(
694 bytes[header_crc_offset..header_crc_offset + 4]
695 .try_into()
696 .ok()?,
697 );
698 if header_crc != crc32(&bytes[start..header_crc_offset]) {
699 return None;
700 }
701 let payload_start = start.checked_add(header_len)?;
702 let end = payload_start.checked_add(payload_len)?;
703 if end > bytes.len() {
704 return None;
705 }
706 let payload = bytes[payload_start..end].to_vec();
707 if crc32(&payload) != payload_crc {
708 return None;
709 }
710 Some(DecodedWalFrame {
711 payload,
712 frame_crc: crc32(&bytes[start..end]),
713 end,
714 })
715}
716
717fn decode_legacy_wal_frame(bytes: &[u8], start: usize) -> Option<DecodedWalFrame> {
718 if bytes.len().checked_sub(start)? < LEGACY_WAL_FRAME_HEADER_BYTES {
719 return None;
720 }
721 let payload_len_offset = start + WAL_FRAME_MAGIC.len();
722 let payload_len = u32::from_le_bytes(
723 bytes[payload_len_offset..payload_len_offset + 4]
724 .try_into()
725 .ok()?,
726 ) as usize;
727 let payload_crc_offset = payload_len_offset + 4;
728 let payload_crc = u32::from_le_bytes(
729 bytes[payload_crc_offset..payload_crc_offset + 4]
730 .try_into()
731 .ok()?,
732 );
733 let payload_start = start.checked_add(LEGACY_WAL_FRAME_HEADER_BYTES)?;
734 let end = payload_start.checked_add(payload_len)?;
735 if end > bytes.len() {
736 return None;
737 }
738 let payload = bytes[payload_start..end].to_vec();
739 if crc32(&payload) != payload_crc {
740 return None;
741 }
742 Some(DecodedWalFrame {
743 payload,
744 frame_crc: crc32(&bytes[start..end]),
745 end,
746 })
747}
748
749fn encode_wal_frame(
750 sequence: u64,
751 previous_frame_crc: u32,
752 payload: &[u8],
753) -> RdbFileResult<(Vec<u8>, u32)> {
754 let payload_len = u32::try_from(payload.len())
755 .map_err(|_| RdbFileError::InvalidOperation("embedded wal payload too large".into()))?;
756 let mut frame = Vec::with_capacity(WAL_FRAME_HEADER_BYTES + payload.len());
757 frame.extend_from_slice(WAL_FRAME_MAGIC);
758 frame.extend_from_slice(&WAL_FRAME_VERSION.to_le_bytes());
759 frame.extend_from_slice(&(WAL_FRAME_HEADER_BYTES as u16).to_le_bytes());
760 frame.extend_from_slice(&sequence.to_le_bytes());
761 frame.extend_from_slice(&payload_len.to_le_bytes());
762 frame.extend_from_slice(&crc32(payload).to_le_bytes());
763 frame.extend_from_slice(&previous_frame_crc.to_le_bytes());
764 let header_crc = crc32(&frame);
765 frame.extend_from_slice(&header_crc.to_le_bytes());
766 frame.extend_from_slice(payload);
767 let frame_crc = crc32(&frame);
768 Ok((frame, frame_crc))
769}
770
771fn encode_superblock(superblock: EmbeddedRdbSuperblock) -> RdbFileResult<Vec<u8>> {
772 let mut bytes = vec![0u8; EMBEDDED_RDB_SUPERBLOCK_SIZE as usize];
773 let mut cursor = 0usize;
774 put_bytes(&mut bytes, &mut cursor, SUPERBLOCK_MAGIC);
775 put_u32(&mut bytes, &mut cursor, SUPERBLOCK_VERSION);
776 put_u8(&mut bytes, &mut cursor, superblock.copy_index);
777 put_u64(&mut bytes, &mut cursor, superblock.generation);
778 put_u32(&mut bytes, &mut cursor, superblock.format_version);
779 put_u64(&mut bytes, &mut cursor, superblock.manifest_offset);
780 put_u64(&mut bytes, &mut cursor, superblock.manifest_len);
781 put_u32(&mut bytes, &mut cursor, superblock.manifest_checksum);
782 put_u64(&mut bytes, &mut cursor, superblock.wal_region_offset);
783 put_u64(&mut bytes, &mut cursor, superblock.wal_region_bytes);
784 put_u64(&mut bytes, &mut cursor, superblock.wal_recovery_boundary);
785 put_u64(&mut bytes, &mut cursor, superblock.wal_live_bytes);
786 put_u64(&mut bytes, &mut cursor, superblock.snapshot_offset);
787 put_u64(&mut bytes, &mut cursor, superblock.snapshot_bytes);
788 put_u32(&mut bytes, &mut cursor, superblock.snapshot_checksum);
789
790 let checksum_offset = bytes.len() - CHECKSUM_LEN;
791 let checksum = crc32(&bytes[..checksum_offset]);
792 bytes[checksum_offset..].copy_from_slice(&checksum.to_le_bytes());
793 Ok(bytes)
794}
795
796fn decode_superblock(copy_index: u8, bytes: &[u8]) -> RdbFileResult<EmbeddedRdbSuperblock> {
797 if bytes.len() != EMBEDDED_RDB_SUPERBLOCK_SIZE as usize {
798 return Err(RdbFileError::InvalidOperation(
799 "invalid embedded superblock size".into(),
800 ));
801 }
802 let checksum_offset = bytes.len() - CHECKSUM_LEN;
803 let stored_checksum = u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap());
804 let computed_checksum = crc32(&bytes[..checksum_offset]);
805 if stored_checksum != computed_checksum {
806 return Err(RdbFileError::InvalidOperation(
807 "embedded superblock checksum mismatch".into(),
808 ));
809 }
810
811 let mut cursor = 0usize;
812 if take_bytes(bytes, &mut cursor, SUPERBLOCK_MAGIC.len())? != SUPERBLOCK_MAGIC {
813 return Err(RdbFileError::InvalidOperation(
814 "invalid embedded superblock magic".into(),
815 ));
816 }
817 let version = take_u32(bytes, &mut cursor)?;
818 if version != SUPERBLOCK_VERSION && version != LEGACY_SUPERBLOCK_VERSION {
819 return Err(RdbFileError::InvalidOperation(format!(
820 "unsupported embedded superblock version {version}"
821 )));
822 }
823 let stored_copy_index = take_u8(bytes, &mut cursor)?;
824 if stored_copy_index != copy_index {
825 return Err(RdbFileError::InvalidOperation(
826 "embedded superblock copy index mismatch".into(),
827 ));
828 }
829
830 let generation = take_u64(bytes, &mut cursor)?;
831 let format_version = take_u32(bytes, &mut cursor)?;
832 let manifest_offset = take_u64(bytes, &mut cursor)?;
833 let manifest_len = take_u64(bytes, &mut cursor)?;
834 let manifest_checksum = take_u32(bytes, &mut cursor)?;
835 let wal_region_offset = take_u64(bytes, &mut cursor)?;
836 let wal_region_bytes = take_u64(bytes, &mut cursor)?;
837 let wal_recovery_boundary = take_u64(bytes, &mut cursor)?;
838 let wal_live_bytes = if version == SUPERBLOCK_VERSION {
839 take_u64(bytes, &mut cursor)?
840 } else {
841 wal_recovery_boundary.saturating_sub(wal_region_offset)
842 };
843
844 Ok(EmbeddedRdbSuperblock {
845 copy_index: stored_copy_index,
846 generation,
847 format_version,
848 manifest_offset,
849 manifest_len,
850 manifest_checksum,
851 wal_region_offset,
852 wal_region_bytes,
853 wal_recovery_boundary,
854 wal_live_bytes,
855 snapshot_offset: take_u64(bytes, &mut cursor)?,
856 snapshot_bytes: take_u64(bytes, &mut cursor)?,
857 snapshot_checksum: take_u32(bytes, &mut cursor)?,
858 checksum: stored_checksum,
859 })
860}
861
862fn encode_manifest(manifest: EmbeddedRdbManifest) -> Vec<u8> {
863 let mut bytes = vec![0u8; 8 + 4 + 8 + 8 + 8 + 8 + 8 + 8 + 4 + 16 + CHECKSUM_LEN];
864 let mut cursor = 0usize;
865 put_bytes(&mut bytes, &mut cursor, MANIFEST_MAGIC);
866 put_u32(&mut bytes, &mut cursor, manifest.version);
867 put_u64(&mut bytes, &mut cursor, manifest.wal_region_offset);
868 put_u64(&mut bytes, &mut cursor, manifest.wal_region_bytes);
869 put_u64(&mut bytes, &mut cursor, manifest.wal_recovery_boundary);
870 put_u64(&mut bytes, &mut cursor, manifest.wal_live_bytes);
871 put_u64(&mut bytes, &mut cursor, manifest.snapshot_offset);
872 put_u64(&mut bytes, &mut cursor, manifest.snapshot_bytes);
873 put_u32(&mut bytes, &mut cursor, manifest.snapshot_checksum);
874 put_u128(&mut bytes, &mut cursor, manifest.created_at_unix_ms);
875
876 let checksum_offset = bytes.len() - CHECKSUM_LEN;
877 let checksum = crc32(&bytes[..checksum_offset]);
878 bytes[checksum_offset..].copy_from_slice(&checksum.to_le_bytes());
879 bytes
880}
881
882fn decode_manifest(bytes: &[u8]) -> RdbFileResult<EmbeddedRdbManifest> {
883 let checksum_offset = bytes
884 .len()
885 .checked_sub(CHECKSUM_LEN)
886 .ok_or_else(|| RdbFileError::InvalidOperation("embedded manifest too short".into()))?;
887 let stored_checksum = u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap());
888 let computed_checksum = crc32(&bytes[..checksum_offset]);
889 if stored_checksum != computed_checksum {
890 return Err(RdbFileError::InvalidOperation(
891 "embedded manifest checksum mismatch".into(),
892 ));
893 }
894
895 let mut cursor = 0usize;
896 if take_bytes(bytes, &mut cursor, MANIFEST_MAGIC.len())? != MANIFEST_MAGIC {
897 return Err(RdbFileError::InvalidOperation(
898 "invalid embedded manifest magic".into(),
899 ));
900 }
901 let version = take_u32(bytes, &mut cursor)?;
902 if version != MANIFEST_VERSION && version != LEGACY_MANIFEST_VERSION {
903 return Err(RdbFileError::InvalidOperation(format!(
904 "unsupported embedded manifest version {version}"
905 )));
906 }
907 let wal_region_offset = take_u64(bytes, &mut cursor)?;
908 let wal_region_bytes = take_u64(bytes, &mut cursor)?;
909 let wal_recovery_boundary = take_u64(bytes, &mut cursor)?;
910 let wal_live_bytes = if version == MANIFEST_VERSION {
911 take_u64(bytes, &mut cursor)?
912 } else {
913 wal_recovery_boundary.saturating_sub(wal_region_offset)
914 };
915 Ok(EmbeddedRdbManifest {
916 version,
917 wal_region_offset,
918 wal_region_bytes,
919 wal_recovery_boundary,
920 wal_live_bytes,
921 snapshot_offset: take_u64(bytes, &mut cursor)?,
922 snapshot_bytes: take_u64(bytes, &mut cursor)?,
923 snapshot_checksum: take_u32(bytes, &mut cursor)?,
924 created_at_unix_ms: take_u128(bytes, &mut cursor)?,
925 checksum: stored_checksum,
926 })
927}
928
929fn trailer_checksum(bytes: &[u8]) -> u32 {
930 let checksum_offset = bytes.len() - CHECKSUM_LEN;
931 u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap())
932}
933
934fn superblock_offset(copy_index: u8) -> RdbFileResult<u64> {
935 match copy_index {
936 0 => Ok(EMBEDDED_RDB_SUPERBLOCK_0_OFFSET),
937 1 => Ok(EMBEDDED_RDB_SUPERBLOCK_1_OFFSET),
938 _ => Err(RdbFileError::InvalidOperation(format!(
939 "invalid embedded superblock copy index {copy_index}"
940 ))),
941 }
942}
943
944fn write_at(file: &mut impl VfsFile, offset: u64, bytes: &[u8]) -> RdbFileResult<()> {
945 file.seek(SeekFrom::Start(offset))?;
946 file.write_all(bytes)?;
947 Ok(())
948}
949
950fn crash_inject(point: &str) {
951 #[cfg(target_arch = "wasm32")]
952 let _ = point;
953 #[cfg(not(target_arch = "wasm32"))]
954 {
955 if std::env::var(CRASH_INJECT_ENV).ok().as_deref() == Some(point) {
956 std::process::exit(173);
957 }
958 if crate::buggify!(CRASH_INJECT_ENV, point) {
959 std::process::exit(173);
960 }
961 }
962}
963
964fn put_bytes(target: &mut [u8], cursor: &mut usize, value: &[u8]) {
965 target[*cursor..*cursor + value.len()].copy_from_slice(value);
966 *cursor += value.len();
967}
968
969fn put_u8(target: &mut [u8], cursor: &mut usize, value: u8) {
970 target[*cursor] = value;
971 *cursor += 1;
972}
973
974fn put_u32(target: &mut [u8], cursor: &mut usize, value: u32) {
975 put_bytes(target, cursor, &value.to_le_bytes());
976}
977
978fn put_u64(target: &mut [u8], cursor: &mut usize, value: u64) {
979 put_bytes(target, cursor, &value.to_le_bytes());
980}
981
982fn put_u128(target: &mut [u8], cursor: &mut usize, value: u128) {
983 put_bytes(target, cursor, &value.to_le_bytes());
984}
985
986fn take_bytes<'a>(bytes: &'a [u8], cursor: &mut usize, len: usize) -> RdbFileResult<&'a [u8]> {
987 let end = cursor.checked_add(len).ok_or_else(|| {
988 RdbFileError::InvalidOperation("embedded artifact cursor overflow".into())
989 })?;
990 if end > bytes.len() {
991 return Err(RdbFileError::InvalidOperation(
992 "embedded artifact truncated".into(),
993 ));
994 }
995 let value = &bytes[*cursor..end];
996 *cursor = end;
997 Ok(value)
998}
999
1000fn take_u8(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u8> {
1001 Ok(take_bytes(bytes, cursor, 1)?[0])
1002}
1003
1004fn take_u32(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u32> {
1005 Ok(u32::from_le_bytes(
1006 take_bytes(bytes, cursor, 4)?.try_into().unwrap(),
1007 ))
1008}
1009
1010fn take_u64(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u64> {
1011 Ok(u64::from_le_bytes(
1012 take_bytes(bytes, cursor, 8)?.try_into().unwrap(),
1013 ))
1014}
1015
1016fn take_u128(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u128> {
1017 Ok(u128::from_le_bytes(
1018 take_bytes(bytes, cursor, 16)?.try_into().unwrap(),
1019 ))
1020}
1021
1022fn create_file(
1023 file: &mut impl VfsFile,
1024 path: &Path,
1025 snapshot: &[u8],
1026 created_at_unix_ms: u128,
1027) -> RdbFileResult<EmbeddedRdbOpen> {
1028 let wal_region_offset = EMBEDDED_RDB_MANIFEST_ZONE_END;
1029 let snapshot_offset = wal_region_offset + WAL_REGION_BYTES;
1030 let manifest = EmbeddedRdbManifest {
1031 version: MANIFEST_VERSION,
1032 wal_region_offset,
1033 wal_region_bytes: WAL_REGION_BYTES,
1034 wal_recovery_boundary: wal_region_offset,
1035 wal_live_bytes: 0,
1036 snapshot_offset,
1037 snapshot_bytes: snapshot.len() as u64,
1038 snapshot_checksum: crc32(snapshot),
1039 created_at_unix_ms,
1040 checksum: 0,
1041 };
1042 let manifest_bytes = encode_manifest(manifest);
1043 let manifest_checksum = trailer_checksum(&manifest_bytes);
1044
1045 file.set_len(snapshot_offset + snapshot.len() as u64)?;
1046 write_at(file, EMBEDDED_RDB_MANIFEST_0_OFFSET, &manifest_bytes)?;
1047 if !snapshot.is_empty() {
1048 write_at(file, snapshot_offset, snapshot)?;
1049 }
1050
1051 let base = EmbeddedRdbSuperblock {
1052 copy_index: 0,
1053 generation: 1,
1054 format_version: DEFAULT_FORMAT_VERSION,
1055 manifest_offset: EMBEDDED_RDB_MANIFEST_0_OFFSET,
1056 manifest_len: manifest_bytes.len() as u64,
1057 manifest_checksum,
1058 wal_region_offset,
1059 wal_region_bytes: WAL_REGION_BYTES,
1060 wal_recovery_boundary: wal_region_offset,
1061 wal_live_bytes: 0,
1062 snapshot_offset,
1063 snapshot_bytes: snapshot.len() as u64,
1064 snapshot_checksum: crc32(snapshot),
1065 checksum: 0,
1066 };
1067 write_superblock_copy(file, &base)?;
1068 write_superblock_copy(
1069 file,
1070 &EmbeddedRdbSuperblock {
1071 copy_index: 1,
1072 generation: 2,
1073 ..base
1074 },
1075 )?;
1076 file.sync_all()?;
1077
1078 EmbeddedRdbArtifact::open_inner_with_file(file, path, true)
1079}
1080
1081fn checkpoint_file(
1082 file: &mut impl VfsFile,
1083 path: &Path,
1084 snapshot: &[u8],
1085 min_wal_bytes: u64,
1086) -> RdbFileResult<EmbeddedRdbOpen> {
1087 let open = EmbeddedRdbArtifact::open_inner_with_file(file, path, true)?;
1088 let wal_scan = match wal_region_start_checked(&open)? {
1089 Some(wal_start) => scan_wal_with_file(file, &open, wal_start)?,
1090 None => WalScan {
1091 next_sequence: 1,
1092 ..WalScan::default()
1093 },
1094 };
1095 let checkpoint_boundary = wal_boundary_after_live_bytes(&open, wal_scan.valid_bytes)?;
1096 let wal_region_bytes = grow_wal_region_bytes(open.manifest.wal_region_bytes, min_wal_bytes)?;
1097 let snapshot_checksum = crc32(snapshot);
1098 if open.manifest.wal_live_bytes == 0
1103 && wal_scan.valid_bytes == 0
1104 && wal_region_bytes == open.manifest.wal_region_bytes
1105 && open.manifest.snapshot_bytes == snapshot.len() as u64
1106 && open.manifest.snapshot_checksum == snapshot_checksum
1107 && published_snapshot_matches(file, &open, snapshot)?
1108 {
1109 return Ok(open);
1110 }
1111 let snapshot_offset = next_snapshot_offset(file, &open, wal_region_bytes, snapshot)?;
1112 let manifest = EmbeddedRdbManifest {
1113 wal_region_bytes,
1114 wal_recovery_boundary: checkpoint_boundary,
1115 wal_live_bytes: 0,
1116 snapshot_offset,
1117 snapshot_bytes: snapshot.len() as u64,
1118 snapshot_checksum,
1119 checksum: 0,
1120 ..open.manifest
1121 };
1122 let manifest_bytes = encode_manifest(manifest);
1123 let manifest_checksum = trailer_checksum(&manifest_bytes);
1124
1125 let snapshot_end = snapshot_offset + snapshot.len() as u64;
1128 if file.file_len()? < snapshot_end {
1129 file.set_len(snapshot_end)?;
1130 }
1131 if !snapshot.is_empty() {
1132 write_at(file, snapshot_offset, snapshot)?;
1133 }
1134 crash_inject("snapshot_after_image_write");
1135 file.sync_data()?;
1136 crash_inject("snapshot_after_image_sync");
1137
1138 let next_manifest_offset = inactive_manifest_offset(open.selected_superblock)?;
1143 write_at(file, next_manifest_offset, &manifest_bytes)?;
1144 crash_inject("snapshot_after_manifest_write");
1145 file.sync_data()?;
1146 crash_inject("snapshot_after_manifest_sync");
1147
1148 let next_copy_index = if open.selected_superblock.copy_index == 0 {
1149 1
1150 } else {
1151 0
1152 };
1153 let next_superblock = EmbeddedRdbSuperblock {
1154 copy_index: next_copy_index,
1155 generation: open.selected_superblock.generation.saturating_add(1),
1156 manifest_offset: next_manifest_offset,
1157 manifest_len: manifest_bytes.len() as u64,
1158 manifest_checksum,
1159 wal_region_bytes,
1160 wal_recovery_boundary: checkpoint_boundary,
1161 wal_live_bytes: 0,
1162 snapshot_offset,
1163 snapshot_bytes: snapshot.len() as u64,
1164 snapshot_checksum,
1165 checksum: 0,
1166 ..open.selected_superblock
1167 };
1168 write_superblock_copy(file, &next_superblock)?;
1169 crash_inject("snapshot_after_superblock_write");
1170 file.sync_all()?;
1171
1172 let retained_end = snapshot_end
1176 .max(open.manifest.snapshot_offset + open.manifest.snapshot_bytes)
1177 .max(open.manifest.wal_region_offset + wal_region_bytes);
1178 if file.file_len()? > retained_end {
1179 file.set_len(retained_end)?;
1180 file.sync_all()?;
1181 }
1182 EmbeddedRdbArtifact::open_inner_with_file(file, path, true)
1183}
1184
1185#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1186struct PublishedAppend {
1187 open: EmbeddedRdbOpen,
1188 generation: u64,
1189 next_sequence: u64,
1190 previous_frame_crc: u32,
1191 valid_bytes: u64,
1192}
1193
1194fn append_file(
1195 file: &mut impl VfsFile,
1196 path: &Path,
1197 payloads: &[Vec<u8>],
1198 open: EmbeddedRdbOpen,
1199 wal_scan: WalScan,
1200 snapshot_validated: bool,
1201) -> RdbFileResult<PublishedAppend> {
1202 if wal_scan.valid_bytes != open.manifest.wal_live_bytes {
1205 return Err(RdbFileError::ZoneUnrecoverable {
1206 zone: "wal",
1207 path: path.to_path_buf(),
1208 });
1209 }
1210 let mut sequence = wal_scan.next_sequence;
1211 let mut previous_frame_crc = wal_scan.previous_frame_crc;
1212 let mut encoded = Vec::new();
1213 for payload in payloads {
1214 let (frame, frame_crc) = encode_wal_frame(sequence, previous_frame_crc, payload)?;
1215 encoded.extend_from_slice(&frame);
1216 previous_frame_crc = frame_crc;
1217 sequence = sequence.saturating_add(1);
1218 }
1219
1220 let encoded_len = encoded.len() as u64;
1221 let free_bytes = open
1222 .manifest
1223 .wal_region_bytes
1224 .checked_sub(wal_scan.valid_bytes)
1225 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal live bytes overflow".into()))?;
1226 if encoded_len > free_bytes {
1227 return Err(RdbFileError::InvalidOperation(
1228 "embedded wal region full".into(),
1229 ));
1230 }
1231 let next_boundary = wal_boundary_after_live_bytes(&open, wal_scan.valid_bytes + encoded_len)?;
1232
1233 write_circular_wal_bytes(file, &open, &encoded)?;
1234 crash_inject("wal_after_frame_write");
1235 file.sync_data()?;
1236 crash_inject("wal_after_frame_sync");
1237
1238 let next_copy_index = if open.selected_superblock.copy_index == 0 {
1239 1
1240 } else {
1241 0
1242 };
1243 let next_superblock = EmbeddedRdbSuperblock {
1244 copy_index: next_copy_index,
1245 generation: open.selected_superblock.generation.saturating_add(1),
1246 wal_recovery_boundary: next_boundary,
1247 wal_live_bytes: wal_scan.valid_bytes + encoded_len,
1248 checksum: 0,
1249 ..open.selected_superblock
1250 };
1251 write_superblock_copy(file, &next_superblock)?;
1252 crash_inject("wal_after_superblock_write");
1253 file.sync_all()?;
1254 let next_open = EmbeddedRdbArtifact::open_inner_with_file(file, path, !snapshot_validated)?;
1261 Ok(PublishedAppend {
1262 open: next_open,
1263 generation: next_superblock.generation,
1264 next_sequence: sequence,
1265 previous_frame_crc,
1266 valid_bytes: wal_scan.valid_bytes + encoded_len,
1267 })
1268}
1269
1270fn read_snapshot_file(
1271 file: &mut impl VfsFile,
1272 open: &EmbeddedRdbOpen,
1273) -> RdbFileResult<Option<Vec<u8>>> {
1274 if open.manifest.snapshot_bytes == 0 {
1275 return Ok(None);
1276 }
1277 let mut bytes = vec![0u8; open.manifest.snapshot_bytes as usize];
1278 file.seek(SeekFrom::Start(open.manifest.snapshot_offset))?;
1279 file.read_exact(&mut bytes)?;
1280 let checksum = crc32(&bytes);
1281 if checksum != open.manifest.snapshot_checksum {
1282 return Err(RdbFileError::InvalidOperation(format!(
1283 "embedded snapshot checksum mismatch: stored {:#010x}, computed {:#010x}",
1284 open.manifest.snapshot_checksum, checksum
1285 )));
1286 }
1287 if bytes.len() >= SNAPSHOT_MAGIC.len() && &bytes[..SNAPSHOT_MAGIC.len()] != SNAPSHOT_MAGIC {
1288 return Err(RdbFileError::InvalidOperation(
1289 "invalid embedded snapshot magic".into(),
1290 ));
1291 }
1292 Ok(Some(bytes))
1293}
1294
1295fn write_superblock_copy(
1296 file: &mut impl VfsFile,
1297 superblock: &EmbeddedRdbSuperblock,
1298) -> RdbFileResult<()> {
1299 let offset = superblock_offset(superblock.copy_index)?;
1300 write_at(file, offset, &encode_superblock(*superblock)?)?;
1301 Ok(())
1302}
1303
1304#[cfg(test)]
1305mod startup_read_tests;