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