Skip to main content

storage/snapshot/
start.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Where a start begins folding: after a snapshot whose line still sits
7//! at its seq, or from genesis with the reason it could not resume
8//! (`crates/storage/spec/Snapshot/Start.lean` §8-28).
9
10use std::path::Path;
11
12use kernel::Seq;
13
14use super::{ChainSnapshot, SnapshotFit, StoredSnapshot, read_snapshot};
15use crate::error::{StorageError, io_err};
16use crate::jsonl::{claimed_seq, complete_lines, ledger_segments_at, segment_first_seq};
17use crate::real_fs::RealFs;
18use crate::vfs::Vfs;
19
20/// Where a start folds from: the state after a snapshot and the lines
21/// past it, or genesis and the reason no snapshot served.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum SnapshotStart {
24    /// The snapshot fits: fold `tail`, the lines after its seq, onto its views.
25    Resume {
26        snapshot: ChainSnapshot,
27        tail: Vec<Vec<u8>>,
28    },
29    /// Fold from genesis. The lines are not carried: the caller streams
30    /// them a segment at a time rather than holding the whole ledger.
31    Whole(WholeFold),
32}
33
34/// Why a start could not resume from the snapshot.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum WholeFold {
37    NoSnapshot,
38    /// The reason `read_snapshot` gave for bytes that are not a snapshot.
39    Damaged(String),
40    /// The fold rules changed since the snapshot was cut.
41    OtherFoldVersion {
42        found: u32,
43    },
44    /// Another line sits at the snapshot's seq.
45    Stale,
46    /// The ledger ends before the snapshot's seq.
47    Missing,
48}
49
50/// Decide where folding the ledger in `ledger_dir` starts, given the
51/// snapshot in `snapshot_dir` and the fold rules' `fold_version`.
52///
53/// Only the segment holding the snapshot's seq and the segments after
54/// it are read; the whole chain is `prove_chain`'s to walk.
55///
56/// # Errors
57/// `StorageError::Io` when a segment or the snapshot cannot be read.
58pub fn start_from_snapshot(
59    ledger_dir: &Path,
60    snapshot_dir: &Path,
61    fold_version: u32,
62) -> Result<SnapshotStart, StorageError> {
63    start_through(&RealFs::new(), ledger_dir, snapshot_dir, fold_version)
64}
65
66/// [`start_from_snapshot`], reading segments through `vfs`: the seam a
67/// test counts what an opening reads through.
68pub(crate) fn start_through(
69    vfs: &dyn Vfs,
70    ledger_dir: &Path,
71    snapshot_dir: &Path,
72    fold_version: u32,
73) -> Result<SnapshotStart, StorageError> {
74    let because = match read_snapshot(snapshot_dir)? {
75        StoredSnapshot::Absent => WholeFold::NoSnapshot,
76        StoredSnapshot::Damaged(reason) => WholeFold::Damaged(reason),
77        StoredSnapshot::Present(snapshot) if snapshot.fold_version() != fold_version => {
78            WholeFold::OtherFoldVersion {
79                found: snapshot.fold_version(),
80            }
81        }
82        StoredSnapshot::Present(snapshot) => return tail_through(vfs, ledger_dir, snapshot),
83    };
84    Ok(SnapshotStart::Whole(because))
85}
86
87/// Where folding goes on after `snapshot`, which the caller has already
88/// read and accepted: the lines after its seq when its line still sits
89/// there, or genesis with the reason it does not.
90///
91/// # Errors
92/// `StorageError::Io` when a segment cannot be read.
93pub fn tail_after(
94    ledger_dir: &Path,
95    snapshot: ChainSnapshot,
96) -> Result<SnapshotStart, StorageError> {
97    tail_through(&RealFs::new(), ledger_dir, snapshot)
98}
99
100fn tail_through(
101    vfs: &dyn Vfs,
102    ledger_dir: &Path,
103    snapshot: ChainSnapshot,
104) -> Result<SnapshotStart, StorageError> {
105    let because = match lines_from_cut(vfs, ledger_dir, snapshot.seq())? {
106        None => WholeFold::Missing,
107        Some(Cut { line_at_seq, tail }) => match snapshot.fit(&line_at_seq) {
108            SnapshotFit::Fits => return Ok(SnapshotStart::Resume { snapshot, tail }),
109            SnapshotFit::Stale => WholeFold::Stale,
110        },
111    };
112    Ok(SnapshotStart::Whole(because))
113}
114
115/// The line a snapshot was cut at, and the lines after it.
116struct Cut {
117    line_at_seq: Vec<u8>,
118    tail: Vec<Vec<u8>>,
119}
120
121/// The line at `seq` and every complete line after it, read from the
122/// segment whose name claims `seq` onward; `None` when no line sits at
123/// `seq`. Within that segment the line is found from the end, and by
124/// counting from the start when the end cannot say (`crates/storage/spec/Snapshot/Start.lean` §8-28).
125fn lines_from_cut(vfs: &dyn Vfs, dir: &Path, seq: Seq) -> Result<Option<Cut>, StorageError> {
126    let segments = ledger_segments_at(dir)?;
127    let Some((at, first)) = segments.iter().enumerate().rev().find_map(|(at, segment)| {
128        first_seq_of(segment)
129            .filter(|first| *first <= seq)
130            .map(|first| (at, first))
131    }) else {
132        return Ok(None);
133    };
134    let mut onward = segments.iter().skip(at);
135    let Some(holding) = onward.next() else {
136        return Ok(None);
137    };
138    let bytes = vfs.read(holding).map_err(io_err("read segment", holding))?;
139    let Some(mut cut) = from_the_end(&bytes, seq).or_else(|| from_the_start(&bytes, first, seq))
140    else {
141        return Ok(None);
142    };
143    for segment in onward {
144        let bytes = vfs.read(segment).map_err(io_err("read segment", segment))?;
145        cut.tail.extend(
146            complete_lines(&bytes)
147                .0
148                .into_iter()
149                .filter(|line| !line.is_empty())
150                .map(<[u8]>::to_vec),
151        );
152    }
153    Ok(Some(cut))
154}
155
156/// The line at `seq` in one segment's `bytes`, and its complete lines
157/// after it, found from the end: the last complete line names its own
158/// seq, so the line at `seq` is that many lines back, and only the bytes
159/// after it are scanned. `None` when the last line names no seq, names
160/// one before `seq`, or the segment holds fewer lines than that.
161fn from_the_end(bytes: &[u8], seq: Seq) -> Option<Cut> {
162    let end = bytes.iter().rposition(|byte| *byte == b'\n')?;
163    let mut lines = bytes
164        .get(..end)?
165        .rsplit(|byte| *byte == b'\n')
166        .filter(|line| !line.is_empty());
167    let newest = lines.clone().next()?;
168    let back = claimed_seq(newest)?.value().checked_sub(seq.value())?;
169    let back = usize::try_from(back).ok()?;
170    let mut tail: Vec<Vec<u8>> = lines.by_ref().take(back).map(<[u8]>::to_vec).collect();
171    if tail.len() != back {
172        return None;
173    }
174    let line_at_seq = lines.next()?.to_vec();
175    tail.reverse();
176    Some(Cut { line_at_seq, tail })
177}
178
179/// The line at `seq` in one segment's `bytes`, whose first line is
180/// `first`, and its complete lines after it, found by counting from the
181/// segment's start; `None` when no line sits at `seq`.
182fn from_the_start(bytes: &[u8], first: Seq, seq: Seq) -> Option<Cut> {
183    // Past the address space the line cannot be in memory either.
184    let skip = usize::try_from(seq.value().checked_sub(first.value())?).ok()?;
185    let mut lines = complete_lines(bytes)
186        .0
187        .into_iter()
188        .filter(|line| !line.is_empty())
189        .skip(skip);
190    let line_at_seq = lines.next()?.to_vec();
191    let tail = lines.map(<[u8]>::to_vec).collect();
192    Some(Cut { line_at_seq, tail })
193}
194
195fn first_seq_of(segment: &Path) -> Option<Seq> {
196    segment
197        .file_name()
198        .and_then(|name| name.to_str())
199        .and_then(segment_first_seq)
200}
201
202#[cfg(test)]
203#[allow(
204    clippy::unwrap_used,
205    clippy::indexing_slicing,
206    clippy::arithmetic_side_effects,
207    clippy::as_conversions,
208    reason = "test code"
209)]
210mod tests;