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