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