Skip to main content

reddb_file/
embedded.rs

1//! Embedded single-file `.rdb` artifact.
2//!
3//! This module models the promoted path where one durable database artifact
4//! carries its superblock pair, internal manifest, WAL reservation, and current
5//! store snapshot inside the `.rdb` file itself.
6
7use 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    /// A named zone of the `.rdb` cannot be trusted, so the store does not
25    /// open. ADR 0074 §2 requires the zone be named and §4 requires the
26    /// operator be pointed at `red salvage` rather than left with a panic.
27    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
69/// The manifest zone is a ping-pong pair, exactly like the superblock zone.
70///
71/// A single in-place manifest could not satisfy "never torn": overwriting it
72/// leaves a window in which the durable superblock still names the old
73/// checksum while the bytes are already the new ones. Publishing into the
74/// *inactive* slot and only then pointing a fresh superblock at it means every
75/// valid superblock always references an intact manifest — pre-update or
76/// post-update, never a mixture.
77pub 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    // Published only after validating this generation's snapshot, or after
155    // appending to an unchanged file with an already validated snapshot.
156    stamp: FileMutationStamp,
157    superblock: EmbeddedRdbSuperblock,
158    next_sequence: u64,
159    previous_frame_crc: u32,
160    valid_bytes: u64,
161}
162
163// The superblock detects cooperating writers/checkpoints. File identity and
164// mutation times also invalidate replacement or in-place edits. On platforms
165// without this identity vocabulary, append keeps the validated full scan.
166#[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    /// Read the store's roots through an already-open handle.
287    ///
288    /// The writer path holds an exclusive lock on the data file and must do
289    /// all of its I/O through that one handle: `fs2` maps onto `LockFileEx`
290    /// on Windows, whose byte-range locks are *mandatory* and scoped to the
291    /// handle that took them, so a second `File::open` of the same path is
292    /// denied with `ERROR_LOCK_VIOLATION` (os error 33). That is what made
293    /// every embedded write fail on Windows while reads kept working
294    /// (issues #2265, #2266). POSIX `flock` is advisory and scoped to the
295    /// open file description, which is why it never reproduced on Linux.
296    ///
297    /// Every helper here seeks explicitly, so sharing one cursor across the
298    /// reads and writes of a single operation is safe.
299    fn open_inner_with_file(
300        file: &mut File,
301        path: &Path,
302        validate_snapshot_refs: bool,
303    ) -> RdbFileResult<EmbeddedRdbOpen> {
304        // An I/O error propagates as an I/O error; only bytes that were read
305        // and failed to decode count towards "the zone is unrecoverable".
306        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            // The manifest a valid superblock names is always intact: it was
324            // fsynced into an inactive slot before this generation existed. A
325            // checksum failure here is therefore bit rot, not a torn update,
326            // and ADR 0074 §2 says it fails the open by name — never falls
327            // back to a stale root that would resurrect superseded state.
328            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        // One handle for the whole operation: it carries the exclusive lock,
378        // and on Windows that lock would deny any *other* handle this module
379        // opened to the same path (see `open_inner_with_file`).
380        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        // Publish the manifest into the slot the live superblock does NOT
418        // reference, and make it durable before any superblock names it. Until
419        // the superblock write below lands, the durable state still roots
420        // through the old manifest slot, which these bytes never touched.
421        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        // Release before re-opening: `Self::open` takes its own handle, and
451        // on Windows it could not open one while this lock is held.
452        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        // The superblock is the WAL authority: appends update it without
471        // rewriting the manifest zone, so a stale manifest wal_live_bytes
472        // would make the scan see an empty region and drop live records.
473        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        // Take before any fallible I/O: a failed append must leave no cached
529        // state that could describe a partially published mutation.
530        let cached_tail = path_guard.tail.take();
531        // Single locked handle for the whole append — see
532        // `write_snapshot_with_wal_capacity` and `open_inner_with_file`.
533        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        // Recovery may salvage a valid prefix, but appending after a corrupt
558        // published frame would acknowledge an unreachable new commit.
559        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        // A warm append writes only WAL bytes and the alternate superblock:
613        // it does not change the snapshot. The matching identity/mutation
614        // stamp above proves no intervening file edit since its validation.
615        // Cache misses (including unsupported platforms) retain the full
616        // checksum pass. Normal open/read/scrub always verify snapshot bytes;
617        // this reuse is not a substitute for detecting bit rot on read.
618        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
656/// The manifest slot the given superblock does not reference.
657///
658/// With two superblock copies and two manifest slots this is always the slot
659/// belonging to the *stale* superblock copy — the same copy the update is about
660/// to overwrite. That coincidence is what makes the pair safe: a crash can only
661/// ever clobber the manifest of a superblock that was already being replaced.
662fn 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    // A superblock that points outside the manifest zone is a misdirected or
685    // forged write; never chase the pointer.
686    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
804/// Validate the WAL geometry recorded in the manifest and return the region
805/// start. Shared by [`scan_wal`] and the writer path so both reject the same
806/// malformed manifests.
807fn 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
830/// Scan the WAL, opening the file for reading. Callers already holding a
831/// handle (the writer path) use [`scan_wal_with_file`] instead.
832fn 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
860/// Scan the WAL through an already-open handle — see
861/// [`EmbeddedRdbArtifact::open_inner_with_file`] for why the writer path
862/// cannot open a second one.
863fn 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}