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 let mut superblocks: Vec<EmbeddedRdbSuperblock> = [
194 read_superblock_copy(file, 0)?,
195 read_superblock_copy(file, 1)?,
196 ]
197 .into_iter()
198 .flatten()
199 .collect();
200 superblocks.sort_by_key(|superblock| std::cmp::Reverse(superblock.generation));
201
202 if superblocks.is_empty() {
203 return Err(RdbFileError::ZoneUnrecoverable {
204 zone: "superblock",
205 path: path.to_path_buf(),
206 });
207 }
208
209 for selected_superblock in superblocks {
210 let mut manifest = read_manifest(file, selected_superblock).map_err(|_| {
216 RdbFileError::ZoneUnrecoverable {
217 zone: "manifest",
218 path: path.to_path_buf(),
219 }
220 })?;
221 manifest.wal_recovery_boundary = selected_superblock.wal_recovery_boundary;
222 manifest.wal_live_bytes = selected_superblock.wal_live_bytes;
223 if validate_snapshot_refs && !snapshot_reference_valid(file, &manifest)? {
224 continue;
225 }
226 return Ok(EmbeddedRdbOpen {
227 path: path.to_path_buf(),
228 selected_superblock,
229 manifest,
230 });
231 }
232
233 Err(RdbFileError::ZoneUnrecoverable {
234 zone: "snapshot",
235 path: path.to_path_buf(),
236 })
237 }
238
239 pub fn wal_payloads_encoded_len(payloads: &[Vec<u8>]) -> RdbFileResult<u64> {
240 let mut len = 0u64;
241 for payload in payloads {
242 let payload_len = u32::try_from(payload.len()).map_err(|_| {
243 RdbFileError::InvalidOperation("embedded wal payload too large".into())
244 })?;
245 let frame_len = WAL_FRAME_HEADER_BYTES as u64 + payload_len as u64;
246 len = len.checked_add(frame_len).ok_or_else(|| {
247 RdbFileError::InvalidOperation("embedded wal encoded length overflow".into())
248 })?;
249 }
250 Ok(len)
251 }
252}
253
254fn read_superblock_copy(
255 file: &mut impl VfsFile,
256 copy_index: u8,
257) -> RdbFileResult<Option<EmbeddedRdbSuperblock>> {
258 let offset = superblock_offset(copy_index)?;
259 let mut bytes = vec![0u8; EMBEDDED_RDB_SUPERBLOCK_SIZE as usize];
260 file.seek(SeekFrom::Start(offset))?;
261 file.read_exact(&mut bytes)?;
262 Ok(decode_superblock(copy_index, &bytes).ok())
263}
264
265fn inactive_manifest_offset(superblock: EmbeddedRdbSuperblock) -> RdbFileResult<u64> {
272 match superblock.manifest_offset {
273 EMBEDDED_RDB_MANIFEST_0_OFFSET => Ok(EMBEDDED_RDB_MANIFEST_1_OFFSET),
274 EMBEDDED_RDB_MANIFEST_1_OFFSET => Ok(EMBEDDED_RDB_MANIFEST_0_OFFSET),
275 other => Err(RdbFileError::InvalidOperation(format!(
276 "superblock names a manifest offset outside the zone: {other}"
277 ))),
278 }
279}
280
281fn read_manifest(
282 file: &mut impl VfsFile,
283 superblock: EmbeddedRdbSuperblock,
284) -> RdbFileResult<EmbeddedRdbManifest> {
285 if superblock.manifest_len < CHECKSUM_LEN as u64
286 || superblock.manifest_len > MANIFEST_REGION_BYTES
287 {
288 return Err(RdbFileError::InvalidOperation(format!(
289 "invalid embedded manifest length {}",
290 superblock.manifest_len
291 )));
292 }
293 if !matches!(
296 superblock.manifest_offset,
297 EMBEDDED_RDB_MANIFEST_0_OFFSET | EMBEDDED_RDB_MANIFEST_1_OFFSET
298 ) {
299 return Err(RdbFileError::InvalidOperation(format!(
300 "superblock names a manifest offset outside the zone: {}",
301 superblock.manifest_offset
302 )));
303 }
304
305 let mut bytes = vec![0u8; superblock.manifest_len as usize];
306 file.seek(SeekFrom::Start(superblock.manifest_offset))?;
307 file.read_exact(&mut bytes)?;
308 let checksum = trailer_checksum(&bytes);
309 if checksum != superblock.manifest_checksum {
310 return Err(RdbFileError::InvalidOperation(format!(
311 "embedded manifest checksum mismatch: stored {:#010x}, computed {:#010x}",
312 superblock.manifest_checksum, checksum
313 )));
314 }
315 decode_manifest(&bytes)
316}
317
318fn snapshot_reference_valid(
319 file: &mut impl VfsFile,
320 manifest: &EmbeddedRdbManifest,
321) -> RdbFileResult<bool> {
322 if manifest.snapshot_bytes == 0 {
323 return Ok(true);
324 }
325 let snapshot_end = manifest
326 .snapshot_offset
327 .checked_add(manifest.snapshot_bytes)
328 .ok_or_else(|| RdbFileError::InvalidOperation("embedded snapshot end overflow".into()))?;
329 if snapshot_end > file.file_len()? {
330 return Ok(false);
331 }
332
333 let mut bytes = vec![0u8; manifest.snapshot_bytes as usize];
334 file.seek(SeekFrom::Start(manifest.snapshot_offset))?;
335 if file.read_exact(&mut bytes).is_err() {
336 return Ok(false);
337 }
338 if crc32(&bytes) != manifest.snapshot_checksum {
339 return Ok(false);
340 }
341 if bytes.len() >= SNAPSHOT_MAGIC.len() && &bytes[..SNAPSHOT_MAGIC.len()] != SNAPSHOT_MAGIC {
342 return Ok(false);
343 }
344 Ok(true)
345}
346
347fn published_snapshot_matches(
350 file: &mut impl VfsFile,
351 open: &EmbeddedRdbOpen,
352 snapshot: &[u8],
353) -> RdbFileResult<bool> {
354 if snapshot.is_empty() {
355 return Ok(true);
356 }
357 let mut published = vec![0u8; snapshot.len()];
358 file.seek(SeekFrom::Start(open.manifest.snapshot_offset))?;
359 file.read_exact(&mut published)?;
360 Ok(published == snapshot)
361}
362
363fn grow_wal_region_bytes(current: u64, min_required: u64) -> RdbFileResult<u64> {
364 let mut next = current.max(WAL_REGION_BYTES);
365 while next < min_required {
366 next = next.checked_mul(2).ok_or_else(|| {
367 RdbFileError::InvalidOperation("embedded wal region size overflow".into())
368 })?;
369 }
370 Ok(next)
371}
372
373fn next_snapshot_offset(
381 file: &mut impl VfsFile,
382 open: &EmbeddedRdbOpen,
383 wal_region_bytes: u64,
384 snapshot: &[u8],
385) -> RdbFileResult<u64> {
386 let base = open
387 .manifest
388 .wal_region_offset
389 .checked_add(wal_region_bytes)
390 .ok_or_else(|| {
391 RdbFileError::InvalidOperation("embedded snapshot offset overflow".into())
392 })?;
393 if open.manifest.snapshot_bytes == 0 && snapshot.is_empty() {
394 return Ok(base);
395 }
396
397 let other_copy = if open.selected_superblock.copy_index == 0 {
398 1
399 } else {
400 0
401 };
402 let mut live = vec![(open.manifest.snapshot_offset, open.manifest.snapshot_bytes)];
403 if let Some(other) = read_superblock_copy(file, other_copy)? {
404 live.push((other.snapshot_offset, other.snapshot_bytes));
405 }
406 let mut live_ranges = Vec::with_capacity(live.len());
407 for (offset, bytes) in live {
408 if bytes == 0 {
409 continue;
410 }
411 let end = offset.checked_add(bytes).ok_or_else(|| {
412 RdbFileError::InvalidOperation("embedded snapshot end overflow".into())
413 })?;
414 live_ranges.push((offset, end));
415 }
416
417 let len = snapshot.len() as u64;
418 let mut candidates = vec![align_up(base, SNAPSHOT_ALIGNMENT)?];
419 for &(_, end) in &live_ranges {
420 candidates.push(align_up(end.max(base), SNAPSHOT_ALIGNMENT)?);
421 }
422 candidates.sort_unstable();
423 for &candidate in &candidates {
424 let end = candidate.checked_add(len).ok_or_else(|| {
425 RdbFileError::InvalidOperation("embedded snapshot end overflow".into())
426 })?;
427 if live_ranges
428 .iter()
429 .all(|&(start, live_end)| end <= start || candidate >= live_end)
430 {
431 return Ok(candidate);
432 }
433 }
434 Ok(candidates[candidates.len() - 1])
436}
437
438fn align_up(value: u64, alignment: u64) -> RdbFileResult<u64> {
439 if alignment == 0 {
440 return Ok(value);
441 }
442 let remainder = value % alignment;
443 if remainder == 0 {
444 return Ok(value);
445 }
446 value
447 .checked_add(alignment - remainder)
448 .ok_or_else(|| RdbFileError::InvalidOperation("embedded alignment overflow".into()))
449}
450
451fn wal_region_start_checked(open: &EmbeddedRdbOpen) -> RdbFileResult<Option<u64>> {
455 let wal_start = open.manifest.wal_region_offset;
456 let wal_end = wal_start
457 .checked_add(open.manifest.wal_region_bytes)
458 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal end overflow".into()))?;
459 let append_boundary = open.manifest.wal_recovery_boundary;
460 if append_boundary < wal_start || append_boundary > wal_end {
461 return Err(RdbFileError::InvalidOperation(format!(
462 "invalid embedded wal boundary {append_boundary}"
463 )));
464 }
465 if open.manifest.wal_live_bytes > open.manifest.wal_region_bytes {
466 return Err(RdbFileError::InvalidOperation(format!(
467 "invalid embedded wal live bytes {}",
468 open.manifest.wal_live_bytes
469 )));
470 }
471 if open.manifest.wal_live_bytes == 0 {
472 return Ok(None);
473 }
474 Ok(Some(wal_start))
475}
476
477fn scan_wal_with_file(
481 file: &mut impl VfsFile,
482 open: &EmbeddedRdbOpen,
483 wal_start: u64,
484) -> RdbFileResult<WalScan> {
485 let file_len = file.file_len()?;
486 if file_len <= wal_start {
487 return Ok(WalScan {
488 next_sequence: 1,
489 ..WalScan::default()
490 });
491 }
492 let bytes = read_circular_wal_bytes(file, open, file_len)?;
493 Ok(scan_wal_bytes(&bytes))
494}
495
496fn read_circular_wal_bytes(
497 file: &mut impl VfsFile,
498 open: &EmbeddedRdbOpen,
499 file_len: u64,
500) -> RdbFileResult<Vec<u8>> {
501 let live_start = wal_live_start_relative(open)?;
502 let region_bytes = open.manifest.wal_region_bytes;
503 let live_bytes = open.manifest.wal_live_bytes;
504 let mut remaining = live_bytes.min(file_len.saturating_sub(open.manifest.wal_region_offset));
505 let mut relative = live_start;
506 let mut bytes = Vec::with_capacity(remaining as usize);
507 while remaining > 0 {
508 let contiguous = (region_bytes - relative).min(remaining);
509 let offset = open.manifest.wal_region_offset + relative;
510 file.seek(SeekFrom::Start(offset))?;
511 let mut chunk = vec![0u8; contiguous as usize];
512 file.read_exact(&mut chunk)?;
513 bytes.extend_from_slice(&chunk);
514 remaining -= contiguous;
515 relative = 0;
516 }
517 Ok(bytes)
518}
519
520fn write_circular_wal_bytes(
521 file: &mut impl VfsFile,
522 open: &EmbeddedRdbOpen,
523 bytes: &[u8],
524) -> RdbFileResult<()> {
525 let mut written = 0usize;
526 let mut relative = wal_append_relative(open)?;
527 while written < bytes.len() {
528 if relative == open.manifest.wal_region_bytes {
529 relative = 0;
530 }
531 let contiguous = (open.manifest.wal_region_bytes - relative) as usize;
532 let chunk_len = contiguous.min(bytes.len() - written);
533 write_at(
534 file,
535 open.manifest.wal_region_offset + relative,
536 &bytes[written..written + chunk_len],
537 )?;
538 written += chunk_len;
539 relative += chunk_len as u64;
540 }
541 Ok(())
542}
543
544fn wal_append_relative(open: &EmbeddedRdbOpen) -> RdbFileResult<u64> {
545 open.manifest
546 .wal_recovery_boundary
547 .checked_sub(open.manifest.wal_region_offset)
548 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal boundary underflow".into()))
549}
550
551fn wal_live_start_relative(open: &EmbeddedRdbOpen) -> RdbFileResult<u64> {
552 let append = wal_append_relative(open)?;
553 let region = open.manifest.wal_region_bytes;
554 if region == 0 {
555 return Err(RdbFileError::InvalidOperation(
556 "embedded wal region is empty".into(),
557 ));
558 }
559 Ok((append + region - (open.manifest.wal_live_bytes % region)) % region)
560}
561
562fn wal_boundary_after_live_bytes(open: &EmbeddedRdbOpen, live_bytes: u64) -> RdbFileResult<u64> {
563 if live_bytes > open.manifest.wal_region_bytes {
564 return Err(RdbFileError::InvalidOperation(format!(
565 "embedded wal live bytes {live_bytes} exceed region size {}",
566 open.manifest.wal_region_bytes
567 )));
568 }
569 let start = if open.manifest.wal_live_bytes == 0 {
570 wal_append_relative(open)?
571 } else {
572 wal_live_start_relative(open)?
573 };
574 let relative = (start + live_bytes) % open.manifest.wal_region_bytes;
575 open.manifest
576 .wal_region_offset
577 .checked_add(relative)
578 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal boundary overflow".into()))
579}
580
581fn scan_wal_bytes(bytes: &[u8]) -> WalScan {
582 let mut scan = WalScan {
583 next_sequence: 1,
584 ..WalScan::default()
585 };
586 let mut cursor = 0usize;
587 while cursor < bytes.len() {
588 let Some(frame) = decode_next_wal_frame(bytes, cursor, &scan) else {
589 break;
590 };
591 scan.payloads.push(frame.payload);
592 scan.next_sequence = scan.next_sequence.saturating_add(1);
593 scan.previous_frame_crc = frame.frame_crc;
594 cursor = frame.end;
595 scan.valid_bytes = cursor as u64;
596 }
597 scan
598}
599
600struct DecodedWalFrame {
601 payload: Vec<u8>,
602 frame_crc: u32,
603 end: usize,
604}
605
606fn decode_next_wal_frame(bytes: &[u8], start: usize, scan: &WalScan) -> Option<DecodedWalFrame> {
607 let remaining = bytes.len().checked_sub(start)?;
608 if remaining < WAL_FRAME_MAGIC.len() {
609 return None;
610 }
611 if &bytes[start..start + WAL_FRAME_MAGIC.len()] != WAL_FRAME_MAGIC {
612 return None;
613 }
614 if remaining < WAL_FRAME_MAGIC.len() + 2 {
615 return None;
616 }
617 let version_offset = start + WAL_FRAME_MAGIC.len();
618 let version = u16::from_le_bytes(bytes[version_offset..version_offset + 2].try_into().ok()?);
619 if version == WAL_FRAME_VERSION {
620 decode_v2_wal_frame(bytes, start, scan)
621 } else {
622 decode_legacy_wal_frame(bytes, start)
623 }
624}
625
626fn decode_v2_wal_frame(bytes: &[u8], start: usize, scan: &WalScan) -> Option<DecodedWalFrame> {
627 if bytes.len().checked_sub(start)? < WAL_FRAME_HEADER_BYTES {
628 return None;
629 }
630 let header_len_offset = start + 10;
631 let header_len = u16::from_le_bytes(
632 bytes[header_len_offset..header_len_offset + 2]
633 .try_into()
634 .ok()?,
635 ) as usize;
636 if header_len != WAL_FRAME_HEADER_BYTES {
637 return None;
638 }
639 let sequence_offset = start + 12;
640 let sequence = u64::from_le_bytes(
641 bytes[sequence_offset..sequence_offset + 8]
642 .try_into()
643 .ok()?,
644 );
645 if sequence != scan.next_sequence {
646 return None;
647 }
648 let payload_len_offset = start + 20;
649 let payload_len = u32::from_le_bytes(
650 bytes[payload_len_offset..payload_len_offset + 4]
651 .try_into()
652 .ok()?,
653 ) as usize;
654 let payload_crc_offset = start + 24;
655 let payload_crc = u32::from_le_bytes(
656 bytes[payload_crc_offset..payload_crc_offset + 4]
657 .try_into()
658 .ok()?,
659 );
660 let previous_frame_crc_offset = start + 28;
661 let previous_frame_crc = u32::from_le_bytes(
662 bytes[previous_frame_crc_offset..previous_frame_crc_offset + 4]
663 .try_into()
664 .ok()?,
665 );
666 if previous_frame_crc != scan.previous_frame_crc {
667 return None;
668 }
669 let header_crc_offset = start + 32;
670 let header_crc = u32::from_le_bytes(
671 bytes[header_crc_offset..header_crc_offset + 4]
672 .try_into()
673 .ok()?,
674 );
675 if header_crc != crc32(&bytes[start..header_crc_offset]) {
676 return None;
677 }
678 let payload_start = start.checked_add(header_len)?;
679 let end = payload_start.checked_add(payload_len)?;
680 if end > bytes.len() {
681 return None;
682 }
683 let payload = bytes[payload_start..end].to_vec();
684 if crc32(&payload) != payload_crc {
685 return None;
686 }
687 Some(DecodedWalFrame {
688 payload,
689 frame_crc: crc32(&bytes[start..end]),
690 end,
691 })
692}
693
694fn decode_legacy_wal_frame(bytes: &[u8], start: usize) -> Option<DecodedWalFrame> {
695 if bytes.len().checked_sub(start)? < LEGACY_WAL_FRAME_HEADER_BYTES {
696 return None;
697 }
698 let payload_len_offset = start + WAL_FRAME_MAGIC.len();
699 let payload_len = u32::from_le_bytes(
700 bytes[payload_len_offset..payload_len_offset + 4]
701 .try_into()
702 .ok()?,
703 ) as usize;
704 let payload_crc_offset = payload_len_offset + 4;
705 let payload_crc = u32::from_le_bytes(
706 bytes[payload_crc_offset..payload_crc_offset + 4]
707 .try_into()
708 .ok()?,
709 );
710 let payload_start = start.checked_add(LEGACY_WAL_FRAME_HEADER_BYTES)?;
711 let end = payload_start.checked_add(payload_len)?;
712 if end > bytes.len() {
713 return None;
714 }
715 let payload = bytes[payload_start..end].to_vec();
716 if crc32(&payload) != payload_crc {
717 return None;
718 }
719 Some(DecodedWalFrame {
720 payload,
721 frame_crc: crc32(&bytes[start..end]),
722 end,
723 })
724}
725
726fn encode_wal_frame(
727 sequence: u64,
728 previous_frame_crc: u32,
729 payload: &[u8],
730) -> RdbFileResult<(Vec<u8>, u32)> {
731 let payload_len = u32::try_from(payload.len())
732 .map_err(|_| RdbFileError::InvalidOperation("embedded wal payload too large".into()))?;
733 let mut frame = Vec::with_capacity(WAL_FRAME_HEADER_BYTES + payload.len());
734 frame.extend_from_slice(WAL_FRAME_MAGIC);
735 frame.extend_from_slice(&WAL_FRAME_VERSION.to_le_bytes());
736 frame.extend_from_slice(&(WAL_FRAME_HEADER_BYTES as u16).to_le_bytes());
737 frame.extend_from_slice(&sequence.to_le_bytes());
738 frame.extend_from_slice(&payload_len.to_le_bytes());
739 frame.extend_from_slice(&crc32(payload).to_le_bytes());
740 frame.extend_from_slice(&previous_frame_crc.to_le_bytes());
741 let header_crc = crc32(&frame);
742 frame.extend_from_slice(&header_crc.to_le_bytes());
743 frame.extend_from_slice(payload);
744 let frame_crc = crc32(&frame);
745 Ok((frame, frame_crc))
746}
747
748fn encode_superblock(superblock: EmbeddedRdbSuperblock) -> RdbFileResult<Vec<u8>> {
749 let mut bytes = vec![0u8; EMBEDDED_RDB_SUPERBLOCK_SIZE as usize];
750 let mut cursor = 0usize;
751 put_bytes(&mut bytes, &mut cursor, SUPERBLOCK_MAGIC);
752 put_u32(&mut bytes, &mut cursor, SUPERBLOCK_VERSION);
753 put_u8(&mut bytes, &mut cursor, superblock.copy_index);
754 put_u64(&mut bytes, &mut cursor, superblock.generation);
755 put_u32(&mut bytes, &mut cursor, superblock.format_version);
756 put_u64(&mut bytes, &mut cursor, superblock.manifest_offset);
757 put_u64(&mut bytes, &mut cursor, superblock.manifest_len);
758 put_u32(&mut bytes, &mut cursor, superblock.manifest_checksum);
759 put_u64(&mut bytes, &mut cursor, superblock.wal_region_offset);
760 put_u64(&mut bytes, &mut cursor, superblock.wal_region_bytes);
761 put_u64(&mut bytes, &mut cursor, superblock.wal_recovery_boundary);
762 put_u64(&mut bytes, &mut cursor, superblock.wal_live_bytes);
763 put_u64(&mut bytes, &mut cursor, superblock.snapshot_offset);
764 put_u64(&mut bytes, &mut cursor, superblock.snapshot_bytes);
765 put_u32(&mut bytes, &mut cursor, superblock.snapshot_checksum);
766
767 let checksum_offset = bytes.len() - CHECKSUM_LEN;
768 let checksum = crc32(&bytes[..checksum_offset]);
769 bytes[checksum_offset..].copy_from_slice(&checksum.to_le_bytes());
770 Ok(bytes)
771}
772
773fn decode_superblock(copy_index: u8, bytes: &[u8]) -> RdbFileResult<EmbeddedRdbSuperblock> {
774 if bytes.len() != EMBEDDED_RDB_SUPERBLOCK_SIZE as usize {
775 return Err(RdbFileError::InvalidOperation(
776 "invalid embedded superblock size".into(),
777 ));
778 }
779 let checksum_offset = bytes.len() - CHECKSUM_LEN;
780 let stored_checksum = u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap());
781 let computed_checksum = crc32(&bytes[..checksum_offset]);
782 if stored_checksum != computed_checksum {
783 return Err(RdbFileError::InvalidOperation(
784 "embedded superblock checksum mismatch".into(),
785 ));
786 }
787
788 let mut cursor = 0usize;
789 if take_bytes(bytes, &mut cursor, SUPERBLOCK_MAGIC.len())? != SUPERBLOCK_MAGIC {
790 return Err(RdbFileError::InvalidOperation(
791 "invalid embedded superblock magic".into(),
792 ));
793 }
794 let version = take_u32(bytes, &mut cursor)?;
795 if version != SUPERBLOCK_VERSION && version != LEGACY_SUPERBLOCK_VERSION {
796 return Err(RdbFileError::InvalidOperation(format!(
797 "unsupported embedded superblock version {version}"
798 )));
799 }
800 let stored_copy_index = take_u8(bytes, &mut cursor)?;
801 if stored_copy_index != copy_index {
802 return Err(RdbFileError::InvalidOperation(
803 "embedded superblock copy index mismatch".into(),
804 ));
805 }
806
807 let generation = take_u64(bytes, &mut cursor)?;
808 let format_version = take_u32(bytes, &mut cursor)?;
809 let manifest_offset = take_u64(bytes, &mut cursor)?;
810 let manifest_len = take_u64(bytes, &mut cursor)?;
811 let manifest_checksum = take_u32(bytes, &mut cursor)?;
812 let wal_region_offset = take_u64(bytes, &mut cursor)?;
813 let wal_region_bytes = take_u64(bytes, &mut cursor)?;
814 let wal_recovery_boundary = take_u64(bytes, &mut cursor)?;
815 let wal_live_bytes = if version == SUPERBLOCK_VERSION {
816 take_u64(bytes, &mut cursor)?
817 } else {
818 wal_recovery_boundary.saturating_sub(wal_region_offset)
819 };
820
821 Ok(EmbeddedRdbSuperblock {
822 copy_index: stored_copy_index,
823 generation,
824 format_version,
825 manifest_offset,
826 manifest_len,
827 manifest_checksum,
828 wal_region_offset,
829 wal_region_bytes,
830 wal_recovery_boundary,
831 wal_live_bytes,
832 snapshot_offset: take_u64(bytes, &mut cursor)?,
833 snapshot_bytes: take_u64(bytes, &mut cursor)?,
834 snapshot_checksum: take_u32(bytes, &mut cursor)?,
835 checksum: stored_checksum,
836 })
837}
838
839fn encode_manifest(manifest: EmbeddedRdbManifest) -> Vec<u8> {
840 let mut bytes = vec![0u8; 8 + 4 + 8 + 8 + 8 + 8 + 8 + 8 + 4 + 16 + CHECKSUM_LEN];
841 let mut cursor = 0usize;
842 put_bytes(&mut bytes, &mut cursor, MANIFEST_MAGIC);
843 put_u32(&mut bytes, &mut cursor, manifest.version);
844 put_u64(&mut bytes, &mut cursor, manifest.wal_region_offset);
845 put_u64(&mut bytes, &mut cursor, manifest.wal_region_bytes);
846 put_u64(&mut bytes, &mut cursor, manifest.wal_recovery_boundary);
847 put_u64(&mut bytes, &mut cursor, manifest.wal_live_bytes);
848 put_u64(&mut bytes, &mut cursor, manifest.snapshot_offset);
849 put_u64(&mut bytes, &mut cursor, manifest.snapshot_bytes);
850 put_u32(&mut bytes, &mut cursor, manifest.snapshot_checksum);
851 put_u128(&mut bytes, &mut cursor, manifest.created_at_unix_ms);
852
853 let checksum_offset = bytes.len() - CHECKSUM_LEN;
854 let checksum = crc32(&bytes[..checksum_offset]);
855 bytes[checksum_offset..].copy_from_slice(&checksum.to_le_bytes());
856 bytes
857}
858
859fn decode_manifest(bytes: &[u8]) -> RdbFileResult<EmbeddedRdbManifest> {
860 let checksum_offset = bytes
861 .len()
862 .checked_sub(CHECKSUM_LEN)
863 .ok_or_else(|| RdbFileError::InvalidOperation("embedded manifest too short".into()))?;
864 let stored_checksum = u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap());
865 let computed_checksum = crc32(&bytes[..checksum_offset]);
866 if stored_checksum != computed_checksum {
867 return Err(RdbFileError::InvalidOperation(
868 "embedded manifest checksum mismatch".into(),
869 ));
870 }
871
872 let mut cursor = 0usize;
873 if take_bytes(bytes, &mut cursor, MANIFEST_MAGIC.len())? != MANIFEST_MAGIC {
874 return Err(RdbFileError::InvalidOperation(
875 "invalid embedded manifest magic".into(),
876 ));
877 }
878 let version = take_u32(bytes, &mut cursor)?;
879 if version != MANIFEST_VERSION && version != LEGACY_MANIFEST_VERSION {
880 return Err(RdbFileError::InvalidOperation(format!(
881 "unsupported embedded manifest version {version}"
882 )));
883 }
884 let wal_region_offset = take_u64(bytes, &mut cursor)?;
885 let wal_region_bytes = take_u64(bytes, &mut cursor)?;
886 let wal_recovery_boundary = take_u64(bytes, &mut cursor)?;
887 let wal_live_bytes = if version == MANIFEST_VERSION {
888 take_u64(bytes, &mut cursor)?
889 } else {
890 wal_recovery_boundary.saturating_sub(wal_region_offset)
891 };
892 Ok(EmbeddedRdbManifest {
893 version,
894 wal_region_offset,
895 wal_region_bytes,
896 wal_recovery_boundary,
897 wal_live_bytes,
898 snapshot_offset: take_u64(bytes, &mut cursor)?,
899 snapshot_bytes: take_u64(bytes, &mut cursor)?,
900 snapshot_checksum: take_u32(bytes, &mut cursor)?,
901 created_at_unix_ms: take_u128(bytes, &mut cursor)?,
902 checksum: stored_checksum,
903 })
904}
905
906fn trailer_checksum(bytes: &[u8]) -> u32 {
907 let checksum_offset = bytes.len() - CHECKSUM_LEN;
908 u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap())
909}
910
911fn superblock_offset(copy_index: u8) -> RdbFileResult<u64> {
912 match copy_index {
913 0 => Ok(EMBEDDED_RDB_SUPERBLOCK_0_OFFSET),
914 1 => Ok(EMBEDDED_RDB_SUPERBLOCK_1_OFFSET),
915 _ => Err(RdbFileError::InvalidOperation(format!(
916 "invalid embedded superblock copy index {copy_index}"
917 ))),
918 }
919}
920
921fn write_at(file: &mut impl VfsFile, offset: u64, bytes: &[u8]) -> RdbFileResult<()> {
922 file.seek(SeekFrom::Start(offset))?;
923 file.write_all(bytes)?;
924 Ok(())
925}
926
927fn crash_inject(point: &str) {
928 #[cfg(target_arch = "wasm32")]
929 let _ = point;
930 #[cfg(not(target_arch = "wasm32"))]
931 {
932 if std::env::var(CRASH_INJECT_ENV).ok().as_deref() == Some(point) {
933 std::process::exit(173);
934 }
935 if crate::buggify!(CRASH_INJECT_ENV, point) {
936 std::process::exit(173);
937 }
938 }
939}
940
941fn put_bytes(target: &mut [u8], cursor: &mut usize, value: &[u8]) {
942 target[*cursor..*cursor + value.len()].copy_from_slice(value);
943 *cursor += value.len();
944}
945
946fn put_u8(target: &mut [u8], cursor: &mut usize, value: u8) {
947 target[*cursor] = value;
948 *cursor += 1;
949}
950
951fn put_u32(target: &mut [u8], cursor: &mut usize, value: u32) {
952 put_bytes(target, cursor, &value.to_le_bytes());
953}
954
955fn put_u64(target: &mut [u8], cursor: &mut usize, value: u64) {
956 put_bytes(target, cursor, &value.to_le_bytes());
957}
958
959fn put_u128(target: &mut [u8], cursor: &mut usize, value: u128) {
960 put_bytes(target, cursor, &value.to_le_bytes());
961}
962
963fn take_bytes<'a>(bytes: &'a [u8], cursor: &mut usize, len: usize) -> RdbFileResult<&'a [u8]> {
964 let end = cursor.checked_add(len).ok_or_else(|| {
965 RdbFileError::InvalidOperation("embedded artifact cursor overflow".into())
966 })?;
967 if end > bytes.len() {
968 return Err(RdbFileError::InvalidOperation(
969 "embedded artifact truncated".into(),
970 ));
971 }
972 let value = &bytes[*cursor..end];
973 *cursor = end;
974 Ok(value)
975}
976
977fn take_u8(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u8> {
978 Ok(take_bytes(bytes, cursor, 1)?[0])
979}
980
981fn take_u32(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u32> {
982 Ok(u32::from_le_bytes(
983 take_bytes(bytes, cursor, 4)?.try_into().unwrap(),
984 ))
985}
986
987fn take_u64(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u64> {
988 Ok(u64::from_le_bytes(
989 take_bytes(bytes, cursor, 8)?.try_into().unwrap(),
990 ))
991}
992
993fn take_u128(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u128> {
994 Ok(u128::from_le_bytes(
995 take_bytes(bytes, cursor, 16)?.try_into().unwrap(),
996 ))
997}
998
999fn create_file(
1000 file: &mut impl VfsFile,
1001 path: &Path,
1002 snapshot: &[u8],
1003 created_at_unix_ms: u128,
1004) -> RdbFileResult<EmbeddedRdbOpen> {
1005 let wal_region_offset = EMBEDDED_RDB_MANIFEST_ZONE_END;
1006 let snapshot_offset = wal_region_offset + WAL_REGION_BYTES;
1007 let manifest = EmbeddedRdbManifest {
1008 version: MANIFEST_VERSION,
1009 wal_region_offset,
1010 wal_region_bytes: WAL_REGION_BYTES,
1011 wal_recovery_boundary: wal_region_offset,
1012 wal_live_bytes: 0,
1013 snapshot_offset,
1014 snapshot_bytes: snapshot.len() as u64,
1015 snapshot_checksum: crc32(snapshot),
1016 created_at_unix_ms,
1017 checksum: 0,
1018 };
1019 let manifest_bytes = encode_manifest(manifest);
1020 let manifest_checksum = trailer_checksum(&manifest_bytes);
1021
1022 file.set_len(snapshot_offset + snapshot.len() as u64)?;
1023 write_at(file, EMBEDDED_RDB_MANIFEST_0_OFFSET, &manifest_bytes)?;
1024 if !snapshot.is_empty() {
1025 write_at(file, snapshot_offset, snapshot)?;
1026 }
1027
1028 let base = EmbeddedRdbSuperblock {
1029 copy_index: 0,
1030 generation: 1,
1031 format_version: DEFAULT_FORMAT_VERSION,
1032 manifest_offset: EMBEDDED_RDB_MANIFEST_0_OFFSET,
1033 manifest_len: manifest_bytes.len() as u64,
1034 manifest_checksum,
1035 wal_region_offset,
1036 wal_region_bytes: WAL_REGION_BYTES,
1037 wal_recovery_boundary: wal_region_offset,
1038 wal_live_bytes: 0,
1039 snapshot_offset,
1040 snapshot_bytes: snapshot.len() as u64,
1041 snapshot_checksum: crc32(snapshot),
1042 checksum: 0,
1043 };
1044 write_superblock_copy(file, &base)?;
1045 write_superblock_copy(
1046 file,
1047 &EmbeddedRdbSuperblock {
1048 copy_index: 1,
1049 generation: 2,
1050 ..base
1051 },
1052 )?;
1053 file.sync_all()?;
1054
1055 EmbeddedRdbArtifact::open_inner_with_file(file, path, true)
1056}
1057
1058fn checkpoint_file(
1059 file: &mut impl VfsFile,
1060 path: &Path,
1061 snapshot: &[u8],
1062 min_wal_bytes: u64,
1063) -> RdbFileResult<EmbeddedRdbOpen> {
1064 let open = EmbeddedRdbArtifact::open_inner_with_file(file, path, true)?;
1065 let wal_scan = match wal_region_start_checked(&open)? {
1066 Some(wal_start) => scan_wal_with_file(file, &open, wal_start)?,
1067 None => WalScan {
1068 next_sequence: 1,
1069 ..WalScan::default()
1070 },
1071 };
1072 let checkpoint_boundary = wal_boundary_after_live_bytes(&open, wal_scan.valid_bytes)?;
1073 let wal_region_bytes = grow_wal_region_bytes(open.manifest.wal_region_bytes, min_wal_bytes)?;
1074 let snapshot_checksum = crc32(snapshot);
1075 if open.manifest.wal_live_bytes == 0
1080 && wal_scan.valid_bytes == 0
1081 && wal_region_bytes == open.manifest.wal_region_bytes
1082 && open.manifest.snapshot_bytes == snapshot.len() as u64
1083 && open.manifest.snapshot_checksum == snapshot_checksum
1084 && published_snapshot_matches(file, &open, snapshot)?
1085 {
1086 return Ok(open);
1087 }
1088 let snapshot_offset = next_snapshot_offset(file, &open, wal_region_bytes, snapshot)?;
1089 let manifest = EmbeddedRdbManifest {
1090 wal_region_bytes,
1091 wal_recovery_boundary: checkpoint_boundary,
1092 wal_live_bytes: 0,
1093 snapshot_offset,
1094 snapshot_bytes: snapshot.len() as u64,
1095 snapshot_checksum,
1096 checksum: 0,
1097 ..open.manifest
1098 };
1099 let manifest_bytes = encode_manifest(manifest);
1100 let manifest_checksum = trailer_checksum(&manifest_bytes);
1101
1102 let snapshot_end = snapshot_offset + snapshot.len() as u64;
1105 if file.file_len()? < snapshot_end {
1106 file.set_len(snapshot_end)?;
1107 }
1108 if !snapshot.is_empty() {
1109 write_at(file, snapshot_offset, snapshot)?;
1110 }
1111 crash_inject("snapshot_after_image_write");
1112 file.sync_data()?;
1113 crash_inject("snapshot_after_image_sync");
1114
1115 let next_manifest_offset = inactive_manifest_offset(open.selected_superblock)?;
1120 write_at(file, next_manifest_offset, &manifest_bytes)?;
1121 crash_inject("snapshot_after_manifest_write");
1122 file.sync_data()?;
1123 crash_inject("snapshot_after_manifest_sync");
1124
1125 let next_copy_index = if open.selected_superblock.copy_index == 0 {
1126 1
1127 } else {
1128 0
1129 };
1130 let next_superblock = EmbeddedRdbSuperblock {
1131 copy_index: next_copy_index,
1132 generation: open.selected_superblock.generation.saturating_add(1),
1133 manifest_offset: next_manifest_offset,
1134 manifest_len: manifest_bytes.len() as u64,
1135 manifest_checksum,
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.selected_superblock
1144 };
1145 write_superblock_copy(file, &next_superblock)?;
1146 crash_inject("snapshot_after_superblock_write");
1147 file.sync_all()?;
1148
1149 let retained_end = snapshot_end
1153 .max(open.manifest.snapshot_offset + open.manifest.snapshot_bytes)
1154 .max(open.manifest.wal_region_offset + wal_region_bytes);
1155 if file.file_len()? > retained_end {
1156 file.set_len(retained_end)?;
1157 file.sync_all()?;
1158 }
1159 EmbeddedRdbArtifact::open_inner_with_file(file, path, true)
1160}
1161
1162#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1163struct PublishedAppend {
1164 open: EmbeddedRdbOpen,
1165 generation: u64,
1166 next_sequence: u64,
1167 previous_frame_crc: u32,
1168 valid_bytes: u64,
1169}
1170
1171fn append_file(
1172 file: &mut impl VfsFile,
1173 path: &Path,
1174 payloads: &[Vec<u8>],
1175 open: EmbeddedRdbOpen,
1176 wal_scan: WalScan,
1177 snapshot_validated: bool,
1178) -> RdbFileResult<PublishedAppend> {
1179 if wal_scan.valid_bytes != open.manifest.wal_live_bytes {
1182 return Err(RdbFileError::ZoneUnrecoverable {
1183 zone: "wal",
1184 path: path.to_path_buf(),
1185 });
1186 }
1187 let mut sequence = wal_scan.next_sequence;
1188 let mut previous_frame_crc = wal_scan.previous_frame_crc;
1189 let mut encoded = Vec::new();
1190 for payload in payloads {
1191 let (frame, frame_crc) = encode_wal_frame(sequence, previous_frame_crc, payload)?;
1192 encoded.extend_from_slice(&frame);
1193 previous_frame_crc = frame_crc;
1194 sequence = sequence.saturating_add(1);
1195 }
1196
1197 let encoded_len = encoded.len() as u64;
1198 let free_bytes = open
1199 .manifest
1200 .wal_region_bytes
1201 .checked_sub(wal_scan.valid_bytes)
1202 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal live bytes overflow".into()))?;
1203 if encoded_len > free_bytes {
1204 return Err(RdbFileError::InvalidOperation(
1205 "embedded wal region full".into(),
1206 ));
1207 }
1208 let next_boundary = wal_boundary_after_live_bytes(&open, wal_scan.valid_bytes + encoded_len)?;
1209
1210 write_circular_wal_bytes(file, &open, &encoded)?;
1211 crash_inject("wal_after_frame_write");
1212 file.sync_data()?;
1213 crash_inject("wal_after_frame_sync");
1214
1215 let next_copy_index = if open.selected_superblock.copy_index == 0 {
1216 1
1217 } else {
1218 0
1219 };
1220 let next_superblock = EmbeddedRdbSuperblock {
1221 copy_index: next_copy_index,
1222 generation: open.selected_superblock.generation.saturating_add(1),
1223 wal_recovery_boundary: next_boundary,
1224 wal_live_bytes: wal_scan.valid_bytes + encoded_len,
1225 checksum: 0,
1226 ..open.selected_superblock
1227 };
1228 write_superblock_copy(file, &next_superblock)?;
1229 crash_inject("wal_after_superblock_write");
1230 file.sync_all()?;
1231 let next_open = EmbeddedRdbArtifact::open_inner_with_file(file, path, !snapshot_validated)?;
1238 Ok(PublishedAppend {
1239 open: next_open,
1240 generation: next_superblock.generation,
1241 next_sequence: sequence,
1242 previous_frame_crc,
1243 valid_bytes: wal_scan.valid_bytes + encoded_len,
1244 })
1245}
1246
1247fn read_snapshot_file(
1248 file: &mut impl VfsFile,
1249 open: &EmbeddedRdbOpen,
1250) -> RdbFileResult<Option<Vec<u8>>> {
1251 if open.manifest.snapshot_bytes == 0 {
1252 return Ok(None);
1253 }
1254 let mut bytes = vec![0u8; open.manifest.snapshot_bytes as usize];
1255 file.seek(SeekFrom::Start(open.manifest.snapshot_offset))?;
1256 file.read_exact(&mut bytes)?;
1257 let checksum = crc32(&bytes);
1258 if checksum != open.manifest.snapshot_checksum {
1259 return Err(RdbFileError::InvalidOperation(format!(
1260 "embedded snapshot checksum mismatch: stored {:#010x}, computed {:#010x}",
1261 open.manifest.snapshot_checksum, checksum
1262 )));
1263 }
1264 if bytes.len() >= SNAPSHOT_MAGIC.len() && &bytes[..SNAPSHOT_MAGIC.len()] != SNAPSHOT_MAGIC {
1265 return Err(RdbFileError::InvalidOperation(
1266 "invalid embedded snapshot magic".into(),
1267 ));
1268 }
1269 Ok(Some(bytes))
1270}
1271
1272fn write_superblock_copy(
1273 file: &mut impl VfsFile,
1274 superblock: &EmbeddedRdbSuperblock,
1275) -> RdbFileResult<()> {
1276 let offset = superblock_offset(superblock.copy_index)?;
1277 write_at(file, offset, &encode_superblock(*superblock)?)?;
1278 Ok(())
1279}