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