1use std::path::{Path, PathBuf};
9
10use kernel::{AxCode, AxError, GENESIS_PREV, Seq, TimeMs};
11
12use crate::chain_audit::ProofCount;
13use crate::error::{StorageError, io_err};
14use crate::real_fs::RealFs;
15use crate::verified_prefix::{ProofRecords, TailStart};
16use crate::vfs::Vfs;
17
18use super::ledger::{
19 JsonlLedger, OpenReport, SEGMENT_PREALLOCATION, SEGMENT_ROLL_BYTES, TailTruncation, WriterLock,
20 complete_lines, is_segment, records_end, segment_file_name, u64_count,
21};
22use super::verify::{LineCheck, LineFault};
23
24impl JsonlLedger {
25 pub fn open(dir: &Path, now: TimeMs) -> Result<(Self, OpenReport), StorageError> {
34 JsonlLedger::open_locked(dir, now, TailProof::Strict)
35 }
36
37 pub fn open_reusing(
45 dir: &Path,
46 now: TimeMs,
47 records: &ProofRecords,
48 ) -> Result<(Self, OpenReport), StorageError> {
49 JsonlLedger::open_locked(dir, now, TailProof::Records(records))
50 }
51
52 fn open_locked(
53 dir: &Path,
54 now: TimeMs,
55 proof: TailProof<'_>,
56 ) -> Result<(Self, OpenReport), StorageError> {
57 let lock = WriterLock::take(dir)?;
58 let (mut ledger, report) =
59 JsonlLedger::open_through(Box::new(RealFs::new()), dir, now, proof)?;
60 ledger.lock = Some(lock);
61 Ok((ledger, report))
62 }
63
64 #[cfg(any(test, feature = "fault"))]
77 pub fn open_faulty(
78 fs: crate::fault_fs::FaultFs,
79 dir: &Path,
80 now: TimeMs,
81 ) -> Result<(Self, OpenReport), StorageError> {
82 JsonlLedger::open_with(Box::new(fs), dir, now)
83 }
84
85 #[cfg(any(test, feature = "fault"))]
88 pub(crate) fn open_with(
89 vfs: Box<dyn Vfs>,
90 dir: &Path,
91 now: TimeMs,
92 ) -> Result<(Self, OpenReport), StorageError> {
93 JsonlLedger::open_through(vfs, dir, now, TailProof::Strict)
94 }
95
96 fn open_through(
97 vfs: Box<dyn Vfs>,
98 dir: &Path,
99 now: TimeMs,
100 proof: TailProof<'_>,
101 ) -> Result<(Self, OpenReport), StorageError> {
102 let (ledger, segments) = JsonlLedger::unopened(vfs, dir)?;
103 ledger.resume(&segments, now, proof)
104 }
105
106 fn unopened(mut vfs: Box<dyn Vfs>, dir: &Path) -> Result<(Self, Vec<PathBuf>), StorageError> {
109 vfs.create_dir_all(dir)
110 .map_err(io_err("create ledger dir", dir))?;
111 let segments: Vec<PathBuf> = vfs
112 .list(dir)
113 .map_err(io_err("list ledger dir", dir))?
114 .into_iter()
115 .filter(|p| is_segment(p))
116 .collect();
117
118 let ledger = JsonlLedger {
119 vfs,
120 dir: dir.to_path_buf(),
121 seg_path: dir.join(segment_file_name(Seq::FIRST)),
122 seg_len: 0,
123 next_seq: Seq::FIRST,
124 prev: GENESIS_PREV,
125 roll_bytes: SEGMENT_ROLL_BYTES,
126 preallocation: SEGMENT_PREALLOCATION,
127 barrier: super::barrier::Barrier::Whole,
128 sessions: crate::sessions::Sessions::for_ledger(dir),
133 observer: None,
134 lock: None,
135 halt: crate::chain_audit::ChainHalt::default(),
136 pending_unwind: None,
137 };
138 Ok((ledger, segments))
139 }
140
141 fn resume(
144 mut self,
145 segments: &[PathBuf],
146 now: TimeMs,
147 proof: TailProof<'_>,
148 ) -> Result<(Self, OpenReport), StorageError> {
149 let Some(last) = segments.last() else {
150 return Ok((
151 self,
152 OpenReport {
153 recovered: None,
154 counted: ProofCount::default(),
155 },
156 ));
157 };
158 self.probe_version(segments)?;
159 let (dropped, counted) = self.recover_tail(segments, last, proof)?;
160 let recovered = if dropped > 0 {
161 if self.next_seq != Seq::FIRST {
162 self.append_log_truncated(now, dropped)?;
163 }
164 Some(TailTruncation {
165 dropped_bytes: dropped,
166 })
167 } else {
168 None
169 };
170 Ok((self, OpenReport { recovered, counted }))
171 }
172
173 fn recover_tail(
178 &mut self,
179 segments: &[PathBuf],
180 last: &Path,
181 proof: TailProof<'_>,
182 ) -> Result<(u64, ProofCount), StorageError> {
183 let boundary = self.boundary(segments, last)?;
184 let prior = boundary.prior;
185 let file = self.vfs.read(last).map_err(io_err("read segment", last))?;
186 let filled = records_end(&file);
190 let total = u64_count(file.len()).map_err(io_err("measure segment", last))?;
191 let mut bytes = file;
192 bytes.truncate(filled);
193 let start = proof.start(
194 last,
195 &bytes,
196 LineCheck::after(boundary.prev, boundary.next_seq),
197 );
198 let mut check = start.check;
199 let mut counted = start.counted;
200 let (lines, _terminated_len) = complete_lines(bytes.get(start.from..).unwrap_or_default());
201
202 let mut valid_len = start.from;
203 for (index, line) in lines.iter().enumerate() {
204 let at = u64_count(index)
205 .map_err(io_err("number a segment line", last))?
206 .saturating_add(1)
207 .saturating_add(start.lines);
208 counted.lines_checked = counted.lines_checked.saturating_add(1);
209 let fault = match check.advance(line) {
210 Ok(_) => {
211 valid_len = valid_len.saturating_add(line.len()).saturating_add(1);
212 continue;
213 }
214 Err(fault) => fault,
215 };
216 let later_intact = || {
227 lines
228 .iter()
229 .skip(index)
230 .any(|l| LineCheck::carries_envelope(l))
231 };
232 let source = match fault {
233 LineFault::VersionAhead(v) => {
234 return Err(StorageError::VersionAhead {
235 path: last.to_path_buf(),
236 v,
237 });
238 }
239 LineFault::NotAVersion(v) => unversioned(v),
240 LineFault::NotALine(_) if !later_intact() => break,
241 LineFault::ChainBreak | LineFault::SeqGap { .. } | LineFault::NotALine(_) => {
242 AxError::failure(
243 AxCode::InvalidArgs,
244 "verify chain",
245 "a line does not continue the chain",
246 )
247 .with_recovery(
248 "run `sprawling replay <ledger-dir>` to see the first line \
249 that breaks, then restore that segment from its \
250 checkpoint commit",
251 )
252 }
253 other @ (LineFault::UnknownKind(_)
254 | LineFault::NotCanonical(_)
255 | LineFault::SeqExhausted(_)) => other.into_ax(at),
256 };
257 return Err(StorageError::Envelope {
258 path: last.to_path_buf(),
259 line: at,
260 source,
261 });
262 }
263 let run_prev = check.prev();
264 let run_seq = check.expected();
265
266 let measure = |count: usize| u64_count(count).map_err(io_err("measure segment", last));
267 let dropped = measure(filled.saturating_sub(valid_len))?;
268 let keep = measure(valid_len)?;
269
270 if self.preallocation.length_after_open(keep, dropped, total) < total {
271 if valid_len == 0 {
272 self.vfs
276 .truncate(last, 0)
277 .map_err(io_err("truncate segment", last))?;
278 self.vfs
279 .sync_data(last)
280 .map_err(io_err("sync segment", last))?;
281 self.vfs
282 .remove_file(last)
283 .map_err(io_err("remove empty segment", last))?;
284 self.vfs
285 .sync_dir(&self.dir.clone())
286 .map_err(io_err("sync ledger dir", &self.dir.clone()))?;
287 match prior {
288 Some(segment) => {
289 self.seg_path = segment.path;
290 self.seg_len = segment.len;
291 }
292 None => {
293 self.seg_path = self.dir.join(segment_file_name(Seq::FIRST));
294 self.seg_len = 0;
295 }
296 }
297 } else {
298 self.vfs
299 .truncate(last, keep)
300 .map_err(io_err("truncate segment", last))?;
301 self.vfs
302 .sync_data(last)
303 .map_err(io_err("sync segment", last))?;
304 self.seg_path = last.to_path_buf();
305 self.seg_len = keep;
306 }
307 } else {
308 self.seg_path = last.to_path_buf();
309 self.seg_len = keep;
310 }
311 self.prev = run_prev;
312 self.next_seq = run_seq;
313 Ok((dropped, counted))
314 }
315}
316
317#[derive(Clone, Copy)]
320enum TailProof<'a> {
321 Strict,
323 Records(&'a ProofRecords),
325}
326
327impl TailProof<'_> {
328 fn start(self, last: &Path, bytes: &[u8], entry: LineCheck) -> TailStart {
331 match (self, last.file_name().and_then(|name| name.to_str())) {
332 (TailProof::Records(records), Some(name)) => records.tail_start(name, bytes, entry),
333 (TailProof::Records(_), None) | (TailProof::Strict, _) => {
334 TailStart::strict(bytes, entry)
335 }
336 }
337 }
338}
339
340pub(super) fn unversioned(v: u64) -> AxError {
345 AxError::failure(
346 AxCode::InvalidArgs,
347 "read a ledger line's version",
348 format!("v{v} is below the first version any build wrote"),
349 )
350 .with_recovery(
351 "restore this segment from its checkpoint commit: the ledger version starts at 1 \
352 and this line declares less",
353 )
354}
355
356#[cfg(test)]
357#[allow(
358 clippy::unwrap_used,
359 clippy::expect_used,
360 clippy::panic,
361 clippy::indexing_slicing,
362 clippy::wildcard_enum_match_arm,
363 clippy::let_underscore_must_use,
364 clippy::let_underscore_untyped,
365 reason = "test code"
366)]
367mod tests;
368
369#[cfg(test)]
370#[allow(clippy::unwrap_used, reason = "test code")]
371mod preallocated;
372
373#[cfg(test)]
374impl JsonlLedger {
375 pub(crate) fn open_preallocated(
379 fs: crate::fault_fs::FaultFs,
380 dir: &Path,
381 now: TimeMs,
382 ) -> Result<(Self, OpenReport), StorageError> {
383 let (mut ledger, segments) = JsonlLedger::unopened(Box::new(fs), dir)?;
384 ledger.preallocation = super::ledger::SegmentPreallocation::ToRollSize;
385 ledger.resume(&segments, now, TailProof::Strict)
386 }
387}