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