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