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