1use std::collections::HashMap;
8use std::fs::{self, File, OpenOptions};
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex, OnceLock};
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use fs2::FileExt;
15
16mod group_commit;
17pub use group_commit::{
18 EmbeddedGroupCommitOptions, EmbeddedGroupCommitSession, PendingEmbeddedAppend,
19};
20
21pub type RdbFileResult<T> = Result<T, RdbFileError>;
22
23pub const DEFAULT_FORMAT_VERSION: u32 = 1;
24
25#[derive(Debug)]
26pub enum RdbFileError {
27 InvalidOperation(String),
28 Io(std::io::Error),
29 ZoneUnrecoverable {
33 zone: &'static str,
34 path: PathBuf,
35 },
36}
37
38impl std::fmt::Display for RdbFileError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Self::InvalidOperation(msg) => write!(f, "INVALID_OPERATION: {msg}"),
42 Self::Io(err) => write!(f, "io error: {err}"),
43 Self::ZoneUnrecoverable { zone, path } => write!(
44 f,
45 "{zone} zone of {} failed validation, so the store will not be opened \
46 (opening it could only return data the zone can no longer vouch for). \
47 Run the `SCRUB` statement to classify the fault and `red salvage` to \
48 extract every entity the damage did not touch; `red salvage` never writes \
49 into the damaged file (ADR 0074 §2/§4).",
50 path.display()
51 ),
52 }
53 }
54}
55
56impl std::error::Error for RdbFileError {}
57
58impl From<std::io::Error> for RdbFileError {
59 fn from(err: std::io::Error) -> Self {
60 Self::Io(err)
61 }
62}
63
64fn crc32(data: &[u8]) -> u32 {
65 let mut hasher = crc32fast::Hasher::new();
66 hasher.update(data);
67 hasher.finalize()
68}
69
70pub const EMBEDDED_RDB_SUPERBLOCK_SIZE: u64 = 4096;
71pub const EMBEDDED_RDB_SUPERBLOCK_0_OFFSET: u64 = 0;
72pub const EMBEDDED_RDB_SUPERBLOCK_1_OFFSET: u64 = EMBEDDED_RDB_SUPERBLOCK_SIZE;
73
74pub const EMBEDDED_RDB_MANIFEST_SLOT_SIZE: u64 = 4096;
83pub const EMBEDDED_RDB_MANIFEST_0_OFFSET: u64 = EMBEDDED_RDB_SUPERBLOCK_SIZE * 2;
84pub const EMBEDDED_RDB_MANIFEST_1_OFFSET: u64 =
85 EMBEDDED_RDB_MANIFEST_0_OFFSET + EMBEDDED_RDB_MANIFEST_SLOT_SIZE;
86pub const EMBEDDED_RDB_MANIFEST_ZONE_END: u64 =
87 EMBEDDED_RDB_MANIFEST_1_OFFSET + EMBEDDED_RDB_MANIFEST_SLOT_SIZE;
88
89const SUPERBLOCK_MAGIC: &[u8; 8] = b"RDBSBLK1";
90const MANIFEST_MAGIC: &[u8; 8] = b"RDBMNFS1";
91const SUPERBLOCK_VERSION: u32 = 2;
92const MANIFEST_VERSION: u32 = 2;
93const LEGACY_SUPERBLOCK_VERSION: u32 = 1;
94const LEGACY_MANIFEST_VERSION: u32 = 1;
95const CHECKSUM_LEN: usize = 4;
96const MANIFEST_REGION_BYTES: u64 = EMBEDDED_RDB_MANIFEST_SLOT_SIZE;
97const WAL_REGION_BYTES: u64 = 64 * 1024;
98const SNAPSHOT_ALIGNMENT: u64 = 4096;
99const SNAPSHOT_MAGIC: &[u8; 4] = b"RDST";
100const WAL_FRAME_MAGIC: &[u8; 8] = b"RDBEWAL1";
101const WAL_FRAME_VERSION: u16 = 2;
102const WAL_FRAME_HEADER_BYTES: usize = 8 + 2 + 2 + 8 + 4 + 4 + 4 + 4;
103const LEGACY_WAL_FRAME_HEADER_BYTES: usize = 8 + 4 + 4;
104const CRASH_INJECT_ENV: &str = "REDDB_EMBEDDED_RDB_CRASH_AT";
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct EmbeddedRdbManifest {
108 pub version: u32,
109 pub wal_region_offset: u64,
110 pub wal_region_bytes: u64,
111 pub wal_recovery_boundary: u64,
112 pub wal_live_bytes: u64,
113 pub snapshot_offset: u64,
114 pub snapshot_bytes: u64,
115 pub snapshot_checksum: u32,
116 pub created_at_unix_ms: u128,
117 pub checksum: u32,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct EmbeddedRdbSuperblock {
122 pub copy_index: u8,
123 pub generation: u64,
124 pub format_version: u32,
125 pub manifest_offset: u64,
126 pub manifest_len: u64,
127 pub manifest_checksum: u32,
128 pub wal_region_offset: u64,
129 pub wal_region_bytes: u64,
130 pub wal_recovery_boundary: u64,
131 pub wal_live_bytes: u64,
132 pub snapshot_offset: u64,
133 pub snapshot_bytes: u64,
134 pub snapshot_checksum: u32,
135 pub checksum: u32,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct EmbeddedRdbOpen {
140 pub path: PathBuf,
141 pub selected_superblock: EmbeddedRdbSuperblock,
142 pub manifest: EmbeddedRdbManifest,
143}
144
145#[derive(Debug, Default)]
146struct WalScan {
147 payloads: Vec<Vec<u8>>,
148 next_sequence: u64,
149 previous_frame_crc: u32,
150 valid_bytes: u64,
151}
152
153#[derive(Default)]
154struct EmbeddedPathState {
155 tail: Option<CachedWalTail>,
156}
157
158struct CachedWalTail {
159 stamp: FileMutationStamp,
162 superblock: EmbeddedRdbSuperblock,
163 next_sequence: u64,
164 previous_frame_crc: u32,
165 valid_bytes: u64,
166}
167
168#[derive(PartialEq, Eq)]
172struct FileMutationStamp {
173 device: u64,
174 inode: u64,
175 len: u64,
176 modified: (i64, i64),
177 changed: (i64, i64),
178}
179
180fn file_mutation_stamp(file: &File) -> std::io::Result<Option<FileMutationStamp>> {
181 #[cfg(unix)]
182 {
183 use std::os::unix::fs::MetadataExt;
184 let metadata = file.metadata()?;
185 Ok(Some(FileMutationStamp {
186 device: metadata.dev(),
187 inode: metadata.ino(),
188 len: metadata.len(),
189 modified: (metadata.mtime(), metadata.mtime_nsec()),
190 changed: (metadata.ctime(), metadata.ctime_nsec()),
191 }))
192 }
193 #[cfg(not(unix))]
194 {
195 let _ = file;
196 Ok(None)
197 }
198}
199
200pub struct EmbeddedRdbWriterLock {
214 #[cfg_attr(not(unix), allow(dead_code))]
215 path: PathBuf,
216 _file: File,
217}
218
219impl EmbeddedRdbWriterLock {
220 pub fn acquire(data_path: impl AsRef<Path>) -> RdbFileResult<Self> {
221 let data_path = data_path.as_ref();
222 let path = crate::layout::embedded_writer_lock_path(data_path);
223 if let Some(parent) = path.parent() {
224 if !parent.as_os_str().is_empty() {
225 fs::create_dir_all(parent)?;
226 }
227 }
228 loop {
229 let file = OpenOptions::new()
230 .create(true)
231 .truncate(false)
232 .read(true)
233 .write(true)
234 .open(&path)?;
235 if let Err(err) = file.try_lock_exclusive() {
236 if err.raw_os_error() == fs2::lock_contended_error().raw_os_error() {
237 return Err(RdbFileError::InvalidOperation(format!(
238 "{} is already open for writing by another process; an embedded \
239 single-file store has exactly one writer process",
240 data_path.display()
241 )));
242 }
243 return Err(err.into());
244 }
245 if lock_file_is_live(&file, &path)? {
249 return Ok(Self { path, _file: file });
250 }
251 }
252 }
253}
254
255impl Drop for EmbeddedRdbWriterLock {
256 fn drop(&mut self) {
257 #[cfg(unix)]
262 {
263 let _ = fs::remove_file(&self.path);
264 }
265 }
266}
267
268fn lock_file_is_live(file: &File, path: &Path) -> RdbFileResult<bool> {
269 #[cfg(unix)]
270 {
271 use std::os::unix::fs::MetadataExt;
272 let held = file.metadata()?;
273 match fs::metadata(path) {
274 Ok(current) => Ok(held.dev() == current.dev() && held.ino() == current.ino()),
275 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
276 Err(err) => Err(err.into()),
277 }
278 }
279 #[cfg(not(unix))]
280 {
281 let _ = (file, path);
282 Ok(true)
283 }
284}
285
286pub struct EmbeddedRdbArtifact;
287
288impl EmbeddedRdbArtifact {
289 pub fn create(path: impl AsRef<Path>) -> RdbFileResult<EmbeddedRdbOpen> {
290 Self::create_with_snapshot(path, &[])
291 }
292
293 pub fn create_with_snapshot(
294 path: impl AsRef<Path>,
295 snapshot: &[u8],
296 ) -> RdbFileResult<EmbeddedRdbOpen> {
297 let path = path.as_ref();
298 if let Some(parent) = path.parent() {
299 if !parent.as_os_str().is_empty() {
300 fs::create_dir_all(parent)?;
301 }
302 }
303
304 let created_at_unix_ms = now_unix_ms();
305 let wal_region_offset = EMBEDDED_RDB_MANIFEST_ZONE_END;
306 let snapshot_offset = wal_region_offset + WAL_REGION_BYTES;
307 let manifest = EmbeddedRdbManifest {
308 version: MANIFEST_VERSION,
309 wal_region_offset,
310 wal_region_bytes: WAL_REGION_BYTES,
311 wal_recovery_boundary: wal_region_offset,
312 wal_live_bytes: 0,
313 snapshot_offset,
314 snapshot_bytes: snapshot.len() as u64,
315 snapshot_checksum: crc32(snapshot),
316 created_at_unix_ms,
317 checksum: 0,
318 };
319 let manifest_bytes = encode_manifest(manifest);
320 let manifest_checksum = trailer_checksum(&manifest_bytes);
321
322 let mut file = OpenOptions::new()
323 .create(true)
324 .truncate(true)
325 .read(true)
326 .write(true)
327 .open(path)?;
328 file.set_len(snapshot_offset + snapshot.len() as u64)?;
329 write_at(&mut file, EMBEDDED_RDB_MANIFEST_0_OFFSET, &manifest_bytes)?;
330 if !snapshot.is_empty() {
331 write_at(&mut file, snapshot_offset, snapshot)?;
332 }
333
334 let base = EmbeddedRdbSuperblock {
335 copy_index: 0,
336 generation: 1,
337 format_version: DEFAULT_FORMAT_VERSION,
338 manifest_offset: EMBEDDED_RDB_MANIFEST_0_OFFSET,
339 manifest_len: manifest_bytes.len() as u64,
340 manifest_checksum,
341 wal_region_offset,
342 wal_region_bytes: WAL_REGION_BYTES,
343 wal_recovery_boundary: wal_region_offset,
344 wal_live_bytes: 0,
345 snapshot_offset,
346 snapshot_bytes: snapshot.len() as u64,
347 snapshot_checksum: crc32(snapshot),
348 checksum: 0,
349 };
350 Self::write_superblock_copy(&mut file, &base)?;
351 Self::write_superblock_copy(
352 &mut file,
353 &EmbeddedRdbSuperblock {
354 copy_index: 1,
355 generation: 2,
356 ..base
357 },
358 )?;
359 file.sync_all()?;
360
361 Self::open(path)
362 }
363
364 pub fn open(path: impl AsRef<Path>) -> RdbFileResult<EmbeddedRdbOpen> {
365 Self::open_inner(path, true)
366 }
367
368 fn open_inner(
369 path: impl AsRef<Path>,
370 validate_snapshot_refs: bool,
371 ) -> RdbFileResult<EmbeddedRdbOpen> {
372 let path = path.as_ref();
373 let mut file = File::open(path)?;
374 FileExt::lock_shared(&file)?;
377 Self::open_inner_with_file(&mut file, path, validate_snapshot_refs)
378 }
379
380 fn open_inner_with_file(
394 file: &mut File,
395 path: &Path,
396 validate_snapshot_refs: bool,
397 ) -> RdbFileResult<EmbeddedRdbOpen> {
398 let mut superblocks: Vec<EmbeddedRdbSuperblock> = [
401 read_superblock_copy(file, 0)?,
402 read_superblock_copy(file, 1)?,
403 ]
404 .into_iter()
405 .flatten()
406 .collect();
407 superblocks.sort_by_key(|superblock| std::cmp::Reverse(superblock.generation));
408
409 if superblocks.is_empty() {
410 return Err(RdbFileError::ZoneUnrecoverable {
411 zone: "superblock",
412 path: path.to_path_buf(),
413 });
414 }
415
416 for selected_superblock in superblocks {
417 let mut manifest = read_manifest(file, selected_superblock).map_err(|_| {
423 RdbFileError::ZoneUnrecoverable {
424 zone: "manifest",
425 path: path.to_path_buf(),
426 }
427 })?;
428 manifest.wal_recovery_boundary = selected_superblock.wal_recovery_boundary;
429 manifest.wal_live_bytes = selected_superblock.wal_live_bytes;
430 if validate_snapshot_refs && !snapshot_reference_valid(file, &manifest)? {
431 continue;
432 }
433 return Ok(EmbeddedRdbOpen {
434 path: path.to_path_buf(),
435 selected_superblock,
436 manifest,
437 });
438 }
439
440 Err(RdbFileError::ZoneUnrecoverable {
441 zone: "snapshot",
442 path: path.to_path_buf(),
443 })
444 }
445
446 pub fn wal_payloads_encoded_len(payloads: &[Vec<u8>]) -> RdbFileResult<u64> {
447 let mut len = 0u64;
448 for payload in payloads {
449 let payload_len = u32::try_from(payload.len()).map_err(|_| {
450 RdbFileError::InvalidOperation("embedded wal payload too large".into())
451 })?;
452 let frame_len = WAL_FRAME_HEADER_BYTES as u64 + payload_len as u64;
453 len = len.checked_add(frame_len).ok_or_else(|| {
454 RdbFileError::InvalidOperation("embedded wal encoded length overflow".into())
455 })?;
456 }
457 Ok(len)
458 }
459
460 pub fn write_snapshot_with_wal_capacity(
461 path: impl AsRef<Path>,
462 snapshot: &[u8],
463 min_wal_bytes: u64,
464 ) -> RdbFileResult<EmbeddedRdbOpen> {
465 let path = path.as_ref();
466 group_commit::wait_for_pending_appends(path)?;
467 let path_lock = embedded_path_lock(path);
468 let mut path_guard = path_lock
469 .lock()
470 .unwrap_or_else(|poisoned| poisoned.into_inner());
471 path_guard.tail = None;
472 let mut file = OpenOptions::new().read(true).write(true).open(path)?;
476 file.lock_exclusive()?;
477
478 let open = Self::open_inner_with_file(&mut file, path, true)?;
479 let wal_scan = match wal_region_start_checked(&open)? {
480 Some(wal_start) => scan_wal_with_file(&mut file, &open, wal_start)?,
481 None => WalScan {
482 next_sequence: 1,
483 ..WalScan::default()
484 },
485 };
486 let checkpoint_boundary = wal_boundary_after_live_bytes(&open, wal_scan.valid_bytes)?;
487 let wal_region_bytes =
488 grow_wal_region_bytes(open.manifest.wal_region_bytes, min_wal_bytes)?;
489 let snapshot_checksum = crc32(snapshot);
490 if open.manifest.wal_live_bytes == 0
495 && wal_scan.valid_bytes == 0
496 && wal_region_bytes == open.manifest.wal_region_bytes
497 && open.manifest.snapshot_bytes == snapshot.len() as u64
498 && open.manifest.snapshot_checksum == snapshot_checksum
499 && published_snapshot_matches(&mut file, &open, snapshot)?
500 {
501 FileExt::unlock(&file)?;
502 return Ok(open);
503 }
504 let snapshot_offset = next_snapshot_offset(&mut file, &open, wal_region_bytes, snapshot)?;
505 let manifest = EmbeddedRdbManifest {
506 wal_region_bytes,
507 wal_recovery_boundary: checkpoint_boundary,
508 wal_live_bytes: 0,
509 snapshot_offset,
510 snapshot_bytes: snapshot.len() as u64,
511 snapshot_checksum,
512 checksum: 0,
513 ..open.manifest
514 };
515 let manifest_bytes = encode_manifest(manifest);
516 let manifest_checksum = trailer_checksum(&manifest_bytes);
517
518 let snapshot_end = snapshot_offset + snapshot.len() as u64;
521 if file.metadata()?.len() < snapshot_end {
522 file.set_len(snapshot_end)?;
523 }
524 if !snapshot.is_empty() {
525 write_at(&mut file, snapshot_offset, snapshot)?;
526 }
527 crash_inject("snapshot_after_image_write");
528 file.sync_data()?;
529 crash_inject("snapshot_after_image_sync");
530
531 let next_manifest_offset = inactive_manifest_offset(open.selected_superblock)?;
536 write_at(&mut file, next_manifest_offset, &manifest_bytes)?;
537 crash_inject("snapshot_after_manifest_write");
538 file.sync_data()?;
539 crash_inject("snapshot_after_manifest_sync");
540
541 let next_copy_index = if open.selected_superblock.copy_index == 0 {
542 1
543 } else {
544 0
545 };
546 let next_superblock = EmbeddedRdbSuperblock {
547 copy_index: next_copy_index,
548 generation: open.selected_superblock.generation.saturating_add(1),
549 manifest_offset: next_manifest_offset,
550 manifest_len: manifest_bytes.len() as u64,
551 manifest_checksum,
552 wal_region_bytes,
553 wal_recovery_boundary: checkpoint_boundary,
554 wal_live_bytes: 0,
555 snapshot_offset,
556 snapshot_bytes: snapshot.len() as u64,
557 snapshot_checksum,
558 checksum: 0,
559 ..open.selected_superblock
560 };
561 Self::write_superblock_copy(&mut file, &next_superblock)?;
562 crash_inject("snapshot_after_superblock_write");
563 file.sync_all()?;
564
565 let retained_end = snapshot_end
569 .max(open.manifest.snapshot_offset + open.manifest.snapshot_bytes)
570 .max(open.manifest.wal_region_offset + wal_region_bytes);
571 if file.metadata()?.len() > retained_end {
572 file.set_len(retained_end)?;
573 file.sync_all()?;
574 }
575 FileExt::unlock(&file)?;
578 Self::open(path)
579 }
580
581 pub fn open_strict_manifest(path: impl AsRef<Path>) -> RdbFileResult<EmbeddedRdbOpen> {
582 let path = path.as_ref();
583 let mut file = File::open(path)?;
584 FileExt::lock_shared(&file)?;
585 let selected_superblock = [
586 read_superblock_copy(&mut file, 0)?,
587 read_superblock_copy(&mut file, 1)?,
588 ]
589 .into_iter()
590 .flatten()
591 .max_by_key(|superblock| superblock.generation)
592 .ok_or_else(|| RdbFileError::InvalidOperation("no valid embedded superblock".into()))?;
593
594 let mut manifest = read_manifest(&mut file, selected_superblock)?;
595 manifest.wal_recovery_boundary = selected_superblock.wal_recovery_boundary;
596 manifest.wal_live_bytes = selected_superblock.wal_live_bytes;
600 Ok(EmbeddedRdbOpen {
601 path: path.to_path_buf(),
602 selected_superblock,
603 manifest,
604 })
605 }
606
607 pub fn read_snapshot(open: &EmbeddedRdbOpen) -> RdbFileResult<Option<Vec<u8>>> {
608 if open.manifest.snapshot_bytes == 0 {
609 return Ok(None);
610 }
611 let mut file = File::open(&open.path)?;
612 FileExt::lock_shared(&file)?;
613 let mut bytes = vec![0u8; open.manifest.snapshot_bytes as usize];
614 file.seek(SeekFrom::Start(open.manifest.snapshot_offset))?;
615 file.read_exact(&mut bytes)?;
616 let checksum = crc32(&bytes);
617 if checksum != open.manifest.snapshot_checksum {
618 return Err(RdbFileError::InvalidOperation(format!(
619 "embedded snapshot checksum mismatch: stored {:#010x}, computed {:#010x}",
620 open.manifest.snapshot_checksum, checksum
621 )));
622 }
623 if bytes.len() >= SNAPSHOT_MAGIC.len() && &bytes[..SNAPSHOT_MAGIC.len()] != SNAPSHOT_MAGIC {
624 return Err(RdbFileError::InvalidOperation(
625 "invalid embedded snapshot magic".into(),
626 ));
627 }
628 Ok(Some(bytes))
629 }
630
631 pub fn write_snapshot(
632 path: impl AsRef<Path>,
633 snapshot: &[u8],
634 ) -> RdbFileResult<EmbeddedRdbOpen> {
635 Self::write_snapshot_with_wal_capacity(path, snapshot, 0)
636 }
637
638 pub fn read_wal_payloads(open: &EmbeddedRdbOpen) -> RdbFileResult<Vec<Vec<u8>>> {
639 Ok(scan_wal(open)?.payloads)
640 }
641
642 pub fn append_wal_payloads(
643 path: impl AsRef<Path>,
644 payloads: &[Vec<u8>],
645 ) -> RdbFileResult<EmbeddedRdbOpen> {
646 let path = path.as_ref();
647 if payloads.is_empty() {
648 return Self::open(path);
649 }
650
651 let path_lock = embedded_path_lock(path);
652 let mut path_guard = path_lock
653 .lock()
654 .unwrap_or_else(|poisoned| poisoned.into_inner());
655 let cached_tail = path_guard.tail.take();
658 let mut file = OpenOptions::new().read(true).write(true).open(path)?;
661 file.lock_exclusive()?;
662
663 let open = Self::open_inner_with_file(&mut file, path, false)?;
664 let stamp = file_mutation_stamp(&file)?;
665 let cached_tail = cached_tail.filter(|tail| {
666 stamp.as_ref() == Some(&tail.stamp) && tail.superblock == open.selected_superblock
667 });
668 let snapshot_validated = cached_tail.is_some();
669 let wal_scan = match cached_tail {
670 Some(tail) => WalScan {
671 next_sequence: tail.next_sequence,
672 previous_frame_crc: tail.previous_frame_crc,
673 valid_bytes: tail.valid_bytes,
674 payloads: Vec::new(),
675 },
676 None => match wal_region_start_checked(&open)? {
677 Some(wal_start) => scan_wal_with_file(&mut file, &open, wal_start)?,
678 None => WalScan {
679 next_sequence: 1,
680 ..WalScan::default()
681 },
682 },
683 };
684 if wal_scan.valid_bytes != open.manifest.wal_live_bytes {
687 return Err(RdbFileError::ZoneUnrecoverable {
688 zone: "wal",
689 path: path.to_path_buf(),
690 });
691 }
692 let mut sequence = wal_scan.next_sequence;
693 let mut previous_frame_crc = wal_scan.previous_frame_crc;
694 let mut encoded = Vec::new();
695 for payload in payloads {
696 let (frame, frame_crc) = encode_wal_frame(sequence, previous_frame_crc, payload)?;
697 encoded.extend_from_slice(&frame);
698 previous_frame_crc = frame_crc;
699 sequence = sequence.saturating_add(1);
700 }
701
702 let encoded_len = encoded.len() as u64;
703 let free_bytes = open
704 .manifest
705 .wal_region_bytes
706 .checked_sub(wal_scan.valid_bytes)
707 .ok_or_else(|| {
708 RdbFileError::InvalidOperation("embedded wal live bytes overflow".into())
709 })?;
710 if encoded_len > free_bytes {
711 return Err(RdbFileError::InvalidOperation(
712 "embedded wal region full".into(),
713 ));
714 }
715 let next_boundary =
716 wal_boundary_after_live_bytes(&open, wal_scan.valid_bytes + encoded_len)?;
717
718 write_circular_wal_bytes(&mut file, &open, &encoded)?;
719 crash_inject("wal_after_frame_write");
720 file.sync_data()?;
721 crash_inject("wal_after_frame_sync");
722
723 let next_copy_index = if open.selected_superblock.copy_index == 0 {
724 1
725 } else {
726 0
727 };
728 let next_superblock = EmbeddedRdbSuperblock {
729 copy_index: next_copy_index,
730 generation: open.selected_superblock.generation.saturating_add(1),
731 wal_recovery_boundary: next_boundary,
732 wal_live_bytes: wal_scan.valid_bytes + encoded_len,
733 checksum: 0,
734 ..open.selected_superblock
735 };
736 Self::write_superblock_copy(&mut file, &next_superblock)?;
737 crash_inject("wal_after_superblock_write");
738 file.sync_all()?;
739 let next_open = Self::open_inner_with_file(&mut file, path, !snapshot_validated)?;
746 let next_stamp = file_mutation_stamp(&file)?;
747 FileExt::unlock(&file)?;
748 if let Some(stamp) = next_stamp
749 .filter(|_| next_open.selected_superblock.generation == next_superblock.generation)
750 {
751 path_guard.tail = Some(CachedWalTail {
752 stamp,
753 superblock: next_open.selected_superblock,
754 next_sequence: sequence,
755 previous_frame_crc,
756 valid_bytes: wal_scan.valid_bytes + encoded_len,
757 });
758 }
759 Ok(next_open)
760 }
761
762 pub fn write_superblock_copy(
763 file: &mut File,
764 superblock: &EmbeddedRdbSuperblock,
765 ) -> RdbFileResult<()> {
766 let offset = superblock_offset(superblock.copy_index)?;
767 write_at(file, offset, &encode_superblock(*superblock)?)?;
768 Ok(())
769 }
770}
771
772fn read_superblock_copy(
773 file: &mut File,
774 copy_index: u8,
775) -> RdbFileResult<Option<EmbeddedRdbSuperblock>> {
776 let offset = superblock_offset(copy_index)?;
777 let mut bytes = vec![0u8; EMBEDDED_RDB_SUPERBLOCK_SIZE as usize];
778 file.seek(SeekFrom::Start(offset))?;
779 file.read_exact(&mut bytes)?;
780 Ok(decode_superblock(copy_index, &bytes).ok())
781}
782
783fn inactive_manifest_offset(superblock: EmbeddedRdbSuperblock) -> RdbFileResult<u64> {
790 match superblock.manifest_offset {
791 EMBEDDED_RDB_MANIFEST_0_OFFSET => Ok(EMBEDDED_RDB_MANIFEST_1_OFFSET),
792 EMBEDDED_RDB_MANIFEST_1_OFFSET => Ok(EMBEDDED_RDB_MANIFEST_0_OFFSET),
793 other => Err(RdbFileError::InvalidOperation(format!(
794 "superblock names a manifest offset outside the zone: {other}"
795 ))),
796 }
797}
798
799fn read_manifest(
800 file: &mut File,
801 superblock: EmbeddedRdbSuperblock,
802) -> RdbFileResult<EmbeddedRdbManifest> {
803 if superblock.manifest_len < CHECKSUM_LEN as u64
804 || superblock.manifest_len > MANIFEST_REGION_BYTES
805 {
806 return Err(RdbFileError::InvalidOperation(format!(
807 "invalid embedded manifest length {}",
808 superblock.manifest_len
809 )));
810 }
811 if !matches!(
814 superblock.manifest_offset,
815 EMBEDDED_RDB_MANIFEST_0_OFFSET | EMBEDDED_RDB_MANIFEST_1_OFFSET
816 ) {
817 return Err(RdbFileError::InvalidOperation(format!(
818 "superblock names a manifest offset outside the zone: {}",
819 superblock.manifest_offset
820 )));
821 }
822
823 let mut bytes = vec![0u8; superblock.manifest_len as usize];
824 file.seek(SeekFrom::Start(superblock.manifest_offset))?;
825 file.read_exact(&mut bytes)?;
826 let checksum = trailer_checksum(&bytes);
827 if checksum != superblock.manifest_checksum {
828 return Err(RdbFileError::InvalidOperation(format!(
829 "embedded manifest checksum mismatch: stored {:#010x}, computed {:#010x}",
830 superblock.manifest_checksum, checksum
831 )));
832 }
833 decode_manifest(&bytes)
834}
835
836fn snapshot_reference_valid(
837 file: &mut File,
838 manifest: &EmbeddedRdbManifest,
839) -> RdbFileResult<bool> {
840 if manifest.snapshot_bytes == 0 {
841 return Ok(true);
842 }
843 let snapshot_end = manifest
844 .snapshot_offset
845 .checked_add(manifest.snapshot_bytes)
846 .ok_or_else(|| RdbFileError::InvalidOperation("embedded snapshot end overflow".into()))?;
847 if snapshot_end > file.metadata()?.len() {
848 return Ok(false);
849 }
850
851 let mut bytes = vec![0u8; manifest.snapshot_bytes as usize];
852 file.seek(SeekFrom::Start(manifest.snapshot_offset))?;
853 if file.read_exact(&mut bytes).is_err() {
854 return Ok(false);
855 }
856 if crc32(&bytes) != manifest.snapshot_checksum {
857 return Ok(false);
858 }
859 if bytes.len() >= SNAPSHOT_MAGIC.len() && &bytes[..SNAPSHOT_MAGIC.len()] != SNAPSHOT_MAGIC {
860 return Ok(false);
861 }
862 Ok(true)
863}
864
865fn published_snapshot_matches(
868 file: &mut File,
869 open: &EmbeddedRdbOpen,
870 snapshot: &[u8],
871) -> RdbFileResult<bool> {
872 if snapshot.is_empty() {
873 return Ok(true);
874 }
875 let mut published = vec![0u8; snapshot.len()];
876 file.seek(SeekFrom::Start(open.manifest.snapshot_offset))?;
877 file.read_exact(&mut published)?;
878 Ok(published == snapshot)
879}
880
881fn grow_wal_region_bytes(current: u64, min_required: u64) -> RdbFileResult<u64> {
882 let mut next = current.max(WAL_REGION_BYTES);
883 while next < min_required {
884 next = next.checked_mul(2).ok_or_else(|| {
885 RdbFileError::InvalidOperation("embedded wal region size overflow".into())
886 })?;
887 }
888 Ok(next)
889}
890
891fn embedded_path_lock(path: &Path) -> Arc<Mutex<EmbeddedPathState>> {
892 static LOCKS: OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<EmbeddedPathState>>>>> =
893 OnceLock::new();
894 let key = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
895 let mut locks = LOCKS
896 .get_or_init(|| Mutex::new(HashMap::new()))
897 .lock()
898 .unwrap_or_else(|poisoned| poisoned.into_inner());
899 locks
900 .entry(key)
901 .or_insert_with(|| Arc::new(Mutex::new(EmbeddedPathState::default())))
902 .clone()
903}
904
905fn next_snapshot_offset(
913 file: &mut File,
914 open: &EmbeddedRdbOpen,
915 wal_region_bytes: u64,
916 snapshot: &[u8],
917) -> RdbFileResult<u64> {
918 let base = open
919 .manifest
920 .wal_region_offset
921 .checked_add(wal_region_bytes)
922 .ok_or_else(|| {
923 RdbFileError::InvalidOperation("embedded snapshot offset overflow".into())
924 })?;
925 if open.manifest.snapshot_bytes == 0 && snapshot.is_empty() {
926 return Ok(base);
927 }
928
929 let other_copy = if open.selected_superblock.copy_index == 0 {
930 1
931 } else {
932 0
933 };
934 let mut live = vec![(open.manifest.snapshot_offset, open.manifest.snapshot_bytes)];
935 if let Some(other) = read_superblock_copy(file, other_copy)? {
936 live.push((other.snapshot_offset, other.snapshot_bytes));
937 }
938 let mut live_ranges = Vec::with_capacity(live.len());
939 for (offset, bytes) in live {
940 if bytes == 0 {
941 continue;
942 }
943 let end = offset.checked_add(bytes).ok_or_else(|| {
944 RdbFileError::InvalidOperation("embedded snapshot end overflow".into())
945 })?;
946 live_ranges.push((offset, end));
947 }
948
949 let len = snapshot.len() as u64;
950 let mut candidates = vec![align_up(base, SNAPSHOT_ALIGNMENT)?];
951 for &(_, end) in &live_ranges {
952 candidates.push(align_up(end.max(base), SNAPSHOT_ALIGNMENT)?);
953 }
954 candidates.sort_unstable();
955 for &candidate in &candidates {
956 let end = candidate.checked_add(len).ok_or_else(|| {
957 RdbFileError::InvalidOperation("embedded snapshot end overflow".into())
958 })?;
959 if live_ranges
960 .iter()
961 .all(|&(start, live_end)| end <= start || candidate >= live_end)
962 {
963 return Ok(candidate);
964 }
965 }
966 Ok(candidates[candidates.len() - 1])
968}
969
970fn align_up(value: u64, alignment: u64) -> RdbFileResult<u64> {
971 if alignment == 0 {
972 return Ok(value);
973 }
974 let remainder = value % alignment;
975 if remainder == 0 {
976 return Ok(value);
977 }
978 value
979 .checked_add(alignment - remainder)
980 .ok_or_else(|| RdbFileError::InvalidOperation("embedded alignment overflow".into()))
981}
982
983fn wal_region_start_checked(open: &EmbeddedRdbOpen) -> RdbFileResult<Option<u64>> {
987 let wal_start = open.manifest.wal_region_offset;
988 let wal_end = wal_start
989 .checked_add(open.manifest.wal_region_bytes)
990 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal end overflow".into()))?;
991 let append_boundary = open.manifest.wal_recovery_boundary;
992 if append_boundary < wal_start || append_boundary > wal_end {
993 return Err(RdbFileError::InvalidOperation(format!(
994 "invalid embedded wal boundary {append_boundary}"
995 )));
996 }
997 if open.manifest.wal_live_bytes > open.manifest.wal_region_bytes {
998 return Err(RdbFileError::InvalidOperation(format!(
999 "invalid embedded wal live bytes {}",
1000 open.manifest.wal_live_bytes
1001 )));
1002 }
1003 if open.manifest.wal_live_bytes == 0 {
1004 return Ok(None);
1005 }
1006 Ok(Some(wal_start))
1007}
1008
1009fn scan_wal(open: &EmbeddedRdbOpen) -> RdbFileResult<WalScan> {
1012 let wal_start = open.manifest.wal_region_offset;
1013 let wal_end = wal_start
1014 .checked_add(open.manifest.wal_region_bytes)
1015 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal end overflow".into()))?;
1016 let append_boundary = open.manifest.wal_recovery_boundary;
1017 if append_boundary < wal_start || append_boundary > wal_end {
1018 return Err(RdbFileError::InvalidOperation(format!(
1019 "invalid embedded wal boundary {append_boundary}"
1020 )));
1021 }
1022 if open.manifest.wal_live_bytes > open.manifest.wal_region_bytes {
1023 return Err(RdbFileError::InvalidOperation(format!(
1024 "invalid embedded wal live bytes {}",
1025 open.manifest.wal_live_bytes
1026 )));
1027 }
1028 if open.manifest.wal_live_bytes == 0 {
1029 return Ok(WalScan {
1030 next_sequence: 1,
1031 ..WalScan::default()
1032 });
1033 }
1034
1035 let mut file = File::open(&open.path)?;
1036 FileExt::lock_shared(&file)?;
1037 scan_wal_with_file(&mut file, open, wal_start)
1038}
1039
1040fn scan_wal_with_file(
1044 file: &mut File,
1045 open: &EmbeddedRdbOpen,
1046 wal_start: u64,
1047) -> RdbFileResult<WalScan> {
1048 let file_len = file.metadata()?.len();
1049 if file_len <= wal_start {
1050 return Ok(WalScan {
1051 next_sequence: 1,
1052 ..WalScan::default()
1053 });
1054 }
1055 let bytes = read_circular_wal_bytes(file, open, file_len)?;
1056 Ok(scan_wal_bytes(&bytes))
1057}
1058
1059fn read_circular_wal_bytes(
1060 file: &mut File,
1061 open: &EmbeddedRdbOpen,
1062 file_len: u64,
1063) -> RdbFileResult<Vec<u8>> {
1064 let live_start = wal_live_start_relative(open)?;
1065 let region_bytes = open.manifest.wal_region_bytes;
1066 let live_bytes = open.manifest.wal_live_bytes;
1067 let mut remaining = live_bytes.min(file_len.saturating_sub(open.manifest.wal_region_offset));
1068 let mut relative = live_start;
1069 let mut bytes = Vec::with_capacity(remaining as usize);
1070 while remaining > 0 {
1071 let contiguous = (region_bytes - relative).min(remaining);
1072 let offset = open.manifest.wal_region_offset + relative;
1073 file.seek(SeekFrom::Start(offset))?;
1074 let mut chunk = vec![0u8; contiguous as usize];
1075 file.read_exact(&mut chunk)?;
1076 bytes.extend_from_slice(&chunk);
1077 remaining -= contiguous;
1078 relative = 0;
1079 }
1080 Ok(bytes)
1081}
1082
1083fn write_circular_wal_bytes(
1084 file: &mut File,
1085 open: &EmbeddedRdbOpen,
1086 bytes: &[u8],
1087) -> RdbFileResult<()> {
1088 let mut written = 0usize;
1089 let mut relative = wal_append_relative(open)?;
1090 while written < bytes.len() {
1091 if relative == open.manifest.wal_region_bytes {
1092 relative = 0;
1093 }
1094 let contiguous = (open.manifest.wal_region_bytes - relative) as usize;
1095 let chunk_len = contiguous.min(bytes.len() - written);
1096 write_at(
1097 file,
1098 open.manifest.wal_region_offset + relative,
1099 &bytes[written..written + chunk_len],
1100 )?;
1101 written += chunk_len;
1102 relative += chunk_len as u64;
1103 }
1104 Ok(())
1105}
1106
1107fn wal_append_relative(open: &EmbeddedRdbOpen) -> RdbFileResult<u64> {
1108 open.manifest
1109 .wal_recovery_boundary
1110 .checked_sub(open.manifest.wal_region_offset)
1111 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal boundary underflow".into()))
1112}
1113
1114fn wal_live_start_relative(open: &EmbeddedRdbOpen) -> RdbFileResult<u64> {
1115 let append = wal_append_relative(open)?;
1116 let region = open.manifest.wal_region_bytes;
1117 if region == 0 {
1118 return Err(RdbFileError::InvalidOperation(
1119 "embedded wal region is empty".into(),
1120 ));
1121 }
1122 Ok((append + region - (open.manifest.wal_live_bytes % region)) % region)
1123}
1124
1125fn wal_boundary_after_live_bytes(open: &EmbeddedRdbOpen, live_bytes: u64) -> RdbFileResult<u64> {
1126 if live_bytes > open.manifest.wal_region_bytes {
1127 return Err(RdbFileError::InvalidOperation(format!(
1128 "embedded wal live bytes {live_bytes} exceed region size {}",
1129 open.manifest.wal_region_bytes
1130 )));
1131 }
1132 let start = if open.manifest.wal_live_bytes == 0 {
1133 wal_append_relative(open)?
1134 } else {
1135 wal_live_start_relative(open)?
1136 };
1137 let relative = (start + live_bytes) % open.manifest.wal_region_bytes;
1138 open.manifest
1139 .wal_region_offset
1140 .checked_add(relative)
1141 .ok_or_else(|| RdbFileError::InvalidOperation("embedded wal boundary overflow".into()))
1142}
1143
1144fn scan_wal_bytes(bytes: &[u8]) -> WalScan {
1145 let mut scan = WalScan {
1146 next_sequence: 1,
1147 ..WalScan::default()
1148 };
1149 let mut cursor = 0usize;
1150 while cursor < bytes.len() {
1151 let Some(frame) = decode_next_wal_frame(bytes, cursor, &scan) else {
1152 break;
1153 };
1154 scan.payloads.push(frame.payload);
1155 scan.next_sequence = scan.next_sequence.saturating_add(1);
1156 scan.previous_frame_crc = frame.frame_crc;
1157 cursor = frame.end;
1158 scan.valid_bytes = cursor as u64;
1159 }
1160 scan
1161}
1162
1163struct DecodedWalFrame {
1164 payload: Vec<u8>,
1165 frame_crc: u32,
1166 end: usize,
1167}
1168
1169fn decode_next_wal_frame(bytes: &[u8], start: usize, scan: &WalScan) -> Option<DecodedWalFrame> {
1170 let remaining = bytes.len().checked_sub(start)?;
1171 if remaining < WAL_FRAME_MAGIC.len() {
1172 return None;
1173 }
1174 if &bytes[start..start + WAL_FRAME_MAGIC.len()] != WAL_FRAME_MAGIC {
1175 return None;
1176 }
1177 if remaining < WAL_FRAME_MAGIC.len() + 2 {
1178 return None;
1179 }
1180 let version_offset = start + WAL_FRAME_MAGIC.len();
1181 let version = u16::from_le_bytes(bytes[version_offset..version_offset + 2].try_into().ok()?);
1182 if version == WAL_FRAME_VERSION {
1183 decode_v2_wal_frame(bytes, start, scan)
1184 } else {
1185 decode_legacy_wal_frame(bytes, start)
1186 }
1187}
1188
1189fn decode_v2_wal_frame(bytes: &[u8], start: usize, scan: &WalScan) -> Option<DecodedWalFrame> {
1190 if bytes.len().checked_sub(start)? < WAL_FRAME_HEADER_BYTES {
1191 return None;
1192 }
1193 let header_len_offset = start + 10;
1194 let header_len = u16::from_le_bytes(
1195 bytes[header_len_offset..header_len_offset + 2]
1196 .try_into()
1197 .ok()?,
1198 ) as usize;
1199 if header_len != WAL_FRAME_HEADER_BYTES {
1200 return None;
1201 }
1202 let sequence_offset = start + 12;
1203 let sequence = u64::from_le_bytes(
1204 bytes[sequence_offset..sequence_offset + 8]
1205 .try_into()
1206 .ok()?,
1207 );
1208 if sequence != scan.next_sequence {
1209 return None;
1210 }
1211 let payload_len_offset = start + 20;
1212 let payload_len = u32::from_le_bytes(
1213 bytes[payload_len_offset..payload_len_offset + 4]
1214 .try_into()
1215 .ok()?,
1216 ) as usize;
1217 let payload_crc_offset = start + 24;
1218 let payload_crc = u32::from_le_bytes(
1219 bytes[payload_crc_offset..payload_crc_offset + 4]
1220 .try_into()
1221 .ok()?,
1222 );
1223 let previous_frame_crc_offset = start + 28;
1224 let previous_frame_crc = u32::from_le_bytes(
1225 bytes[previous_frame_crc_offset..previous_frame_crc_offset + 4]
1226 .try_into()
1227 .ok()?,
1228 );
1229 if previous_frame_crc != scan.previous_frame_crc {
1230 return None;
1231 }
1232 let header_crc_offset = start + 32;
1233 let header_crc = u32::from_le_bytes(
1234 bytes[header_crc_offset..header_crc_offset + 4]
1235 .try_into()
1236 .ok()?,
1237 );
1238 if header_crc != crc32(&bytes[start..header_crc_offset]) {
1239 return None;
1240 }
1241 let payload_start = start.checked_add(header_len)?;
1242 let end = payload_start.checked_add(payload_len)?;
1243 if end > bytes.len() {
1244 return None;
1245 }
1246 let payload = bytes[payload_start..end].to_vec();
1247 if crc32(&payload) != payload_crc {
1248 return None;
1249 }
1250 Some(DecodedWalFrame {
1251 payload,
1252 frame_crc: crc32(&bytes[start..end]),
1253 end,
1254 })
1255}
1256
1257fn decode_legacy_wal_frame(bytes: &[u8], start: usize) -> Option<DecodedWalFrame> {
1258 if bytes.len().checked_sub(start)? < LEGACY_WAL_FRAME_HEADER_BYTES {
1259 return None;
1260 }
1261 let payload_len_offset = start + WAL_FRAME_MAGIC.len();
1262 let payload_len = u32::from_le_bytes(
1263 bytes[payload_len_offset..payload_len_offset + 4]
1264 .try_into()
1265 .ok()?,
1266 ) as usize;
1267 let payload_crc_offset = payload_len_offset + 4;
1268 let payload_crc = u32::from_le_bytes(
1269 bytes[payload_crc_offset..payload_crc_offset + 4]
1270 .try_into()
1271 .ok()?,
1272 );
1273 let payload_start = start.checked_add(LEGACY_WAL_FRAME_HEADER_BYTES)?;
1274 let end = payload_start.checked_add(payload_len)?;
1275 if end > bytes.len() {
1276 return None;
1277 }
1278 let payload = bytes[payload_start..end].to_vec();
1279 if crc32(&payload) != payload_crc {
1280 return None;
1281 }
1282 Some(DecodedWalFrame {
1283 payload,
1284 frame_crc: crc32(&bytes[start..end]),
1285 end,
1286 })
1287}
1288
1289fn encode_wal_frame(
1290 sequence: u64,
1291 previous_frame_crc: u32,
1292 payload: &[u8],
1293) -> RdbFileResult<(Vec<u8>, u32)> {
1294 let payload_len = u32::try_from(payload.len())
1295 .map_err(|_| RdbFileError::InvalidOperation("embedded wal payload too large".into()))?;
1296 let mut frame = Vec::with_capacity(WAL_FRAME_HEADER_BYTES + payload.len());
1297 frame.extend_from_slice(WAL_FRAME_MAGIC);
1298 frame.extend_from_slice(&WAL_FRAME_VERSION.to_le_bytes());
1299 frame.extend_from_slice(&(WAL_FRAME_HEADER_BYTES as u16).to_le_bytes());
1300 frame.extend_from_slice(&sequence.to_le_bytes());
1301 frame.extend_from_slice(&payload_len.to_le_bytes());
1302 frame.extend_from_slice(&crc32(payload).to_le_bytes());
1303 frame.extend_from_slice(&previous_frame_crc.to_le_bytes());
1304 let header_crc = crc32(&frame);
1305 frame.extend_from_slice(&header_crc.to_le_bytes());
1306 frame.extend_from_slice(payload);
1307 let frame_crc = crc32(&frame);
1308 Ok((frame, frame_crc))
1309}
1310
1311fn encode_superblock(superblock: EmbeddedRdbSuperblock) -> RdbFileResult<Vec<u8>> {
1312 let mut bytes = vec![0u8; EMBEDDED_RDB_SUPERBLOCK_SIZE as usize];
1313 let mut cursor = 0usize;
1314 put_bytes(&mut bytes, &mut cursor, SUPERBLOCK_MAGIC);
1315 put_u32(&mut bytes, &mut cursor, SUPERBLOCK_VERSION);
1316 put_u8(&mut bytes, &mut cursor, superblock.copy_index);
1317 put_u64(&mut bytes, &mut cursor, superblock.generation);
1318 put_u32(&mut bytes, &mut cursor, superblock.format_version);
1319 put_u64(&mut bytes, &mut cursor, superblock.manifest_offset);
1320 put_u64(&mut bytes, &mut cursor, superblock.manifest_len);
1321 put_u32(&mut bytes, &mut cursor, superblock.manifest_checksum);
1322 put_u64(&mut bytes, &mut cursor, superblock.wal_region_offset);
1323 put_u64(&mut bytes, &mut cursor, superblock.wal_region_bytes);
1324 put_u64(&mut bytes, &mut cursor, superblock.wal_recovery_boundary);
1325 put_u64(&mut bytes, &mut cursor, superblock.wal_live_bytes);
1326 put_u64(&mut bytes, &mut cursor, superblock.snapshot_offset);
1327 put_u64(&mut bytes, &mut cursor, superblock.snapshot_bytes);
1328 put_u32(&mut bytes, &mut cursor, superblock.snapshot_checksum);
1329
1330 let checksum_offset = bytes.len() - CHECKSUM_LEN;
1331 let checksum = crc32(&bytes[..checksum_offset]);
1332 bytes[checksum_offset..].copy_from_slice(&checksum.to_le_bytes());
1333 Ok(bytes)
1334}
1335
1336fn decode_superblock(copy_index: u8, bytes: &[u8]) -> RdbFileResult<EmbeddedRdbSuperblock> {
1337 if bytes.len() != EMBEDDED_RDB_SUPERBLOCK_SIZE as usize {
1338 return Err(RdbFileError::InvalidOperation(
1339 "invalid embedded superblock size".into(),
1340 ));
1341 }
1342 let checksum_offset = bytes.len() - CHECKSUM_LEN;
1343 let stored_checksum = u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap());
1344 let computed_checksum = crc32(&bytes[..checksum_offset]);
1345 if stored_checksum != computed_checksum {
1346 return Err(RdbFileError::InvalidOperation(
1347 "embedded superblock checksum mismatch".into(),
1348 ));
1349 }
1350
1351 let mut cursor = 0usize;
1352 if take_bytes(bytes, &mut cursor, SUPERBLOCK_MAGIC.len())? != SUPERBLOCK_MAGIC {
1353 return Err(RdbFileError::InvalidOperation(
1354 "invalid embedded superblock magic".into(),
1355 ));
1356 }
1357 let version = take_u32(bytes, &mut cursor)?;
1358 if version != SUPERBLOCK_VERSION && version != LEGACY_SUPERBLOCK_VERSION {
1359 return Err(RdbFileError::InvalidOperation(format!(
1360 "unsupported embedded superblock version {version}"
1361 )));
1362 }
1363 let stored_copy_index = take_u8(bytes, &mut cursor)?;
1364 if stored_copy_index != copy_index {
1365 return Err(RdbFileError::InvalidOperation(
1366 "embedded superblock copy index mismatch".into(),
1367 ));
1368 }
1369
1370 let generation = take_u64(bytes, &mut cursor)?;
1371 let format_version = take_u32(bytes, &mut cursor)?;
1372 let manifest_offset = take_u64(bytes, &mut cursor)?;
1373 let manifest_len = take_u64(bytes, &mut cursor)?;
1374 let manifest_checksum = take_u32(bytes, &mut cursor)?;
1375 let wal_region_offset = take_u64(bytes, &mut cursor)?;
1376 let wal_region_bytes = take_u64(bytes, &mut cursor)?;
1377 let wal_recovery_boundary = take_u64(bytes, &mut cursor)?;
1378 let wal_live_bytes = if version == SUPERBLOCK_VERSION {
1379 take_u64(bytes, &mut cursor)?
1380 } else {
1381 wal_recovery_boundary.saturating_sub(wal_region_offset)
1382 };
1383
1384 Ok(EmbeddedRdbSuperblock {
1385 copy_index: stored_copy_index,
1386 generation,
1387 format_version,
1388 manifest_offset,
1389 manifest_len,
1390 manifest_checksum,
1391 wal_region_offset,
1392 wal_region_bytes,
1393 wal_recovery_boundary,
1394 wal_live_bytes,
1395 snapshot_offset: take_u64(bytes, &mut cursor)?,
1396 snapshot_bytes: take_u64(bytes, &mut cursor)?,
1397 snapshot_checksum: take_u32(bytes, &mut cursor)?,
1398 checksum: stored_checksum,
1399 })
1400}
1401
1402fn encode_manifest(manifest: EmbeddedRdbManifest) -> Vec<u8> {
1403 let mut bytes = vec![0u8; 8 + 4 + 8 + 8 + 8 + 8 + 8 + 8 + 4 + 16 + CHECKSUM_LEN];
1404 let mut cursor = 0usize;
1405 put_bytes(&mut bytes, &mut cursor, MANIFEST_MAGIC);
1406 put_u32(&mut bytes, &mut cursor, manifest.version);
1407 put_u64(&mut bytes, &mut cursor, manifest.wal_region_offset);
1408 put_u64(&mut bytes, &mut cursor, manifest.wal_region_bytes);
1409 put_u64(&mut bytes, &mut cursor, manifest.wal_recovery_boundary);
1410 put_u64(&mut bytes, &mut cursor, manifest.wal_live_bytes);
1411 put_u64(&mut bytes, &mut cursor, manifest.snapshot_offset);
1412 put_u64(&mut bytes, &mut cursor, manifest.snapshot_bytes);
1413 put_u32(&mut bytes, &mut cursor, manifest.snapshot_checksum);
1414 put_u128(&mut bytes, &mut cursor, manifest.created_at_unix_ms);
1415
1416 let checksum_offset = bytes.len() - CHECKSUM_LEN;
1417 let checksum = crc32(&bytes[..checksum_offset]);
1418 bytes[checksum_offset..].copy_from_slice(&checksum.to_le_bytes());
1419 bytes
1420}
1421
1422fn decode_manifest(bytes: &[u8]) -> RdbFileResult<EmbeddedRdbManifest> {
1423 let checksum_offset = bytes
1424 .len()
1425 .checked_sub(CHECKSUM_LEN)
1426 .ok_or_else(|| RdbFileError::InvalidOperation("embedded manifest too short".into()))?;
1427 let stored_checksum = u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap());
1428 let computed_checksum = crc32(&bytes[..checksum_offset]);
1429 if stored_checksum != computed_checksum {
1430 return Err(RdbFileError::InvalidOperation(
1431 "embedded manifest checksum mismatch".into(),
1432 ));
1433 }
1434
1435 let mut cursor = 0usize;
1436 if take_bytes(bytes, &mut cursor, MANIFEST_MAGIC.len())? != MANIFEST_MAGIC {
1437 return Err(RdbFileError::InvalidOperation(
1438 "invalid embedded manifest magic".into(),
1439 ));
1440 }
1441 let version = take_u32(bytes, &mut cursor)?;
1442 if version != MANIFEST_VERSION && version != LEGACY_MANIFEST_VERSION {
1443 return Err(RdbFileError::InvalidOperation(format!(
1444 "unsupported embedded manifest version {version}"
1445 )));
1446 }
1447 let wal_region_offset = take_u64(bytes, &mut cursor)?;
1448 let wal_region_bytes = take_u64(bytes, &mut cursor)?;
1449 let wal_recovery_boundary = take_u64(bytes, &mut cursor)?;
1450 let wal_live_bytes = if version == MANIFEST_VERSION {
1451 take_u64(bytes, &mut cursor)?
1452 } else {
1453 wal_recovery_boundary.saturating_sub(wal_region_offset)
1454 };
1455 Ok(EmbeddedRdbManifest {
1456 version,
1457 wal_region_offset,
1458 wal_region_bytes,
1459 wal_recovery_boundary,
1460 wal_live_bytes,
1461 snapshot_offset: take_u64(bytes, &mut cursor)?,
1462 snapshot_bytes: take_u64(bytes, &mut cursor)?,
1463 snapshot_checksum: take_u32(bytes, &mut cursor)?,
1464 created_at_unix_ms: take_u128(bytes, &mut cursor)?,
1465 checksum: stored_checksum,
1466 })
1467}
1468
1469fn trailer_checksum(bytes: &[u8]) -> u32 {
1470 let checksum_offset = bytes.len() - CHECKSUM_LEN;
1471 u32::from_le_bytes(bytes[checksum_offset..].try_into().unwrap())
1472}
1473
1474fn superblock_offset(copy_index: u8) -> RdbFileResult<u64> {
1475 match copy_index {
1476 0 => Ok(EMBEDDED_RDB_SUPERBLOCK_0_OFFSET),
1477 1 => Ok(EMBEDDED_RDB_SUPERBLOCK_1_OFFSET),
1478 _ => Err(RdbFileError::InvalidOperation(format!(
1479 "invalid embedded superblock copy index {copy_index}"
1480 ))),
1481 }
1482}
1483
1484fn write_at(file: &mut File, offset: u64, bytes: &[u8]) -> RdbFileResult<()> {
1485 file.seek(SeekFrom::Start(offset))?;
1486 file.write_all(bytes)?;
1487 Ok(())
1488}
1489
1490fn crash_inject(point: &str) {
1491 if std::env::var(CRASH_INJECT_ENV).ok().as_deref() == Some(point) {
1492 std::process::exit(173);
1493 }
1494 if crate::buggify!(CRASH_INJECT_ENV, point) {
1495 std::process::exit(173);
1496 }
1497}
1498
1499fn now_unix_ms() -> u128 {
1500 SystemTime::now()
1501 .duration_since(UNIX_EPOCH)
1502 .map(|duration| duration.as_millis())
1503 .unwrap_or(0)
1504}
1505
1506fn put_bytes(target: &mut [u8], cursor: &mut usize, value: &[u8]) {
1507 target[*cursor..*cursor + value.len()].copy_from_slice(value);
1508 *cursor += value.len();
1509}
1510
1511fn put_u8(target: &mut [u8], cursor: &mut usize, value: u8) {
1512 target[*cursor] = value;
1513 *cursor += 1;
1514}
1515
1516fn put_u32(target: &mut [u8], cursor: &mut usize, value: u32) {
1517 put_bytes(target, cursor, &value.to_le_bytes());
1518}
1519
1520fn put_u64(target: &mut [u8], cursor: &mut usize, value: u64) {
1521 put_bytes(target, cursor, &value.to_le_bytes());
1522}
1523
1524fn put_u128(target: &mut [u8], cursor: &mut usize, value: u128) {
1525 put_bytes(target, cursor, &value.to_le_bytes());
1526}
1527
1528fn take_bytes<'a>(bytes: &'a [u8], cursor: &mut usize, len: usize) -> RdbFileResult<&'a [u8]> {
1529 let end = cursor.checked_add(len).ok_or_else(|| {
1530 RdbFileError::InvalidOperation("embedded artifact cursor overflow".into())
1531 })?;
1532 if end > bytes.len() {
1533 return Err(RdbFileError::InvalidOperation(
1534 "embedded artifact truncated".into(),
1535 ));
1536 }
1537 let value = &bytes[*cursor..end];
1538 *cursor = end;
1539 Ok(value)
1540}
1541
1542fn take_u8(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u8> {
1543 Ok(take_bytes(bytes, cursor, 1)?[0])
1544}
1545
1546fn take_u32(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u32> {
1547 Ok(u32::from_le_bytes(
1548 take_bytes(bytes, cursor, 4)?.try_into().unwrap(),
1549 ))
1550}
1551
1552fn take_u64(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u64> {
1553 Ok(u64::from_le_bytes(
1554 take_bytes(bytes, cursor, 8)?.try_into().unwrap(),
1555 ))
1556}
1557
1558fn take_u128(bytes: &[u8], cursor: &mut usize) -> RdbFileResult<u128> {
1559 Ok(u128::from_le_bytes(
1560 take_bytes(bytes, cursor, 16)?.try_into().unwrap(),
1561 ))
1562}