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