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