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