1use std::path::{Path, PathBuf};
9
10use kernel::consts_external::{LogVersion, readable_log_v};
11use kernel::{AxCode, AxError, GENESIS_PREV, Seq, TimeMs};
12
13use crate::chain_audit::ProofCount;
14use crate::error::{StorageError, io_err};
15use crate::real_fs::RealFs;
16use crate::verified_prefix::{ProofRecords, TailStart};
17use crate::vfs::Vfs;
18
19use super::first_line::first_line;
20use super::ledger::{
21 JsonlLedger, OpenReport, SEGMENT_ROLL_BYTES, TailTruncation, WriterLock, complete_lines,
22 is_segment, segment_file_name,
23};
24use super::verify::{LineCheck, LineFault};
25
26impl JsonlLedger {
27 pub fn open(dir: &Path, now: TimeMs) -> Result<(Self, OpenReport), StorageError> {
36 JsonlLedger::open_locked(dir, now, TailProof::Strict)
37 }
38
39 pub fn open_reusing(
47 dir: &Path,
48 now: TimeMs,
49 records: &ProofRecords,
50 ) -> Result<(Self, OpenReport), StorageError> {
51 JsonlLedger::open_locked(dir, now, TailProof::Records(records))
52 }
53
54 fn open_locked(
55 dir: &Path,
56 now: TimeMs,
57 proof: TailProof<'_>,
58 ) -> Result<(Self, OpenReport), StorageError> {
59 let lock = WriterLock::take(dir)?;
60 let (mut ledger, report) =
61 JsonlLedger::open_through(Box::new(RealFs::new()), dir, now, proof)?;
62 ledger.lock = Some(lock);
63 Ok((ledger, report))
64 }
65
66 #[cfg(any(test, feature = "fault"))]
79 pub fn open_faulty(
80 fs: crate::fault_fs::FaultFs,
81 dir: &Path,
82 now: TimeMs,
83 ) -> Result<(Self, OpenReport), StorageError> {
84 JsonlLedger::open_with(Box::new(fs), dir, now)
85 }
86
87 #[cfg(any(test, feature = "fault"))]
90 pub(crate) fn open_with(
91 vfs: Box<dyn Vfs>,
92 dir: &Path,
93 now: TimeMs,
94 ) -> Result<(Self, OpenReport), StorageError> {
95 JsonlLedger::open_through(vfs, dir, now, TailProof::Strict)
96 }
97
98 fn open_through(
99 mut vfs: Box<dyn Vfs>,
100 dir: &Path,
101 now: TimeMs,
102 proof: TailProof<'_>,
103 ) -> Result<(Self, OpenReport), StorageError> {
104 vfs.create_dir_all(dir)
105 .map_err(io_err("create ledger dir", dir))?;
106 let segments: Vec<PathBuf> = vfs
107 .list(dir)
108 .map_err(io_err("list ledger dir", dir))?
109 .into_iter()
110 .filter(|p| is_segment(p))
111 .collect();
112
113 let mut ledger = JsonlLedger {
114 vfs,
115 dir: dir.to_path_buf(),
116 seg_path: dir.join(segment_file_name(Seq::FIRST)),
117 seg_len: 0,
118 next_seq: Seq::FIRST,
119 prev: GENESIS_PREV,
120 roll_bytes: SEGMENT_ROLL_BYTES,
121 barrier: super::barrier::Barrier::Whole,
122 sessions: crate::sessions::Sessions::for_ledger(dir),
127 observer: None,
128 lock: None,
129 halt: crate::chain_audit::ChainHalt::default(),
130 pending_unwind: None,
131 };
132
133 let Some(last) = segments.last().cloned() else {
134 return Ok((
135 ledger,
136 OpenReport {
137 recovered: None,
138 counted: ProofCount::default(),
139 },
140 ));
141 };
142
143 ledger.probe_version(&segments)?;
144 let (dropped, counted) = ledger.recover_tail(&segments, &last, proof)?;
145 let recovered = if dropped > 0 {
146 if ledger.next_seq != Seq::FIRST {
147 ledger.append_log_truncated(now, dropped)?;
148 }
149 Some(TailTruncation {
150 dropped_bytes: dropped,
151 })
152 } else {
153 None
154 };
155 Ok((ledger, OpenReport { recovered, counted }))
156 }
157
158 fn probe_version(&mut self, segments: &[PathBuf]) -> Result<(), StorageError> {
161 let Some(first) = segments.first() else {
162 return Ok(());
163 };
164 let Some(first_line) =
165 first_line(self.vfs.as_ref(), first).map_err(io_err("read segment", first))?
166 else {
167 return Ok(());
170 };
171 let probed = serde_json::from_slice::<serde_json::Value>(&first_line)
176 .ok()
177 .and_then(|value| value.get("v").and_then(serde_json::Value::as_u64));
178 let v = match probed {
179 Some(v) => v,
180 None if segments.len() > 1 => {
181 return Err(StorageError::Envelope {
182 path: first.clone(),
183 line: 1,
184 source: AxError::failure(
185 AxCode::InvalidArgs,
186 "probe ledger version",
187 "first line is not a version-bearing record",
188 )
189 .with_recovery(
190 "restore this segment from its checkpoint commit, then run \
191 `sprawling replay <ledger-dir>`: line 1 of every segment carries `v`",
192 ),
193 });
194 }
195 None => return Ok(()),
196 };
197 match readable_log_v(v) {
198 LogVersion::Current | LogVersion::Older => Ok(()),
201 LogVersion::Ahead => Err(StorageError::VersionAhead {
202 path: first.clone(),
203 v,
204 }),
205 LogVersion::NotAVersion => Err(StorageError::Envelope {
206 path: first.clone(),
207 line: 1,
208 source: unversioned(v),
209 }),
210 }
211 }
212
213 fn recover_tail(
218 &mut self,
219 segments: &[PathBuf],
220 last: &Path,
221 proof: TailProof<'_>,
222 ) -> Result<(u64, ProofCount), StorageError> {
223 let boundary = self.boundary(segments, last)?;
224 let prior = boundary.prior;
225 let bytes = self.vfs.read(last).map_err(io_err("read segment", last))?;
226 let start = proof.start(
227 last,
228 &bytes,
229 LineCheck::after(boundary.prev, boundary.next_seq),
230 );
231 let mut check = start.check;
232 let mut counted = start.counted;
233 let (lines, _terminated_len) = complete_lines(bytes.get(start.from..).unwrap_or_default());
234
235 let mut valid_len = start.from;
236 for (index, line) in lines.iter().enumerate() {
237 let at = u64::try_from(index)
238 .unwrap_or(u64::MAX)
239 .saturating_add(1)
240 .saturating_add(start.lines);
241 counted.lines_checked = counted.lines_checked.saturating_add(1);
242 let fault = match check.advance(line) {
243 Ok(_) => {
244 valid_len = valid_len.saturating_add(line.len()).saturating_add(1);
245 continue;
246 }
247 Err(fault) => fault,
248 };
249 let later_intact = || {
258 lines
259 .iter()
260 .skip(index.saturating_add(1))
261 .any(|l| LineCheck::carries_envelope(l))
262 };
263 let source = match fault {
264 LineFault::VersionAhead(v) => {
265 return Err(StorageError::VersionAhead {
266 path: last.to_path_buf(),
267 v,
268 });
269 }
270 LineFault::NotAVersion(v) => unversioned(v),
271 LineFault::NotALine(_) if !later_intact() => break,
272 LineFault::ChainBreak | LineFault::SeqGap { .. } | LineFault::NotALine(_) => {
273 AxError::failure(
274 AxCode::InvalidArgs,
275 "verify chain",
276 "a line does not continue the chain",
277 )
278 .with_recovery(
279 "run `sprawling replay <ledger-dir>` to see the first line \
280 that breaks, then restore that segment from its \
281 checkpoint commit",
282 )
283 }
284 other @ (LineFault::UnknownKind(_)
285 | LineFault::NotCanonical(_)
286 | LineFault::SeqExhausted(_)) => other.into_ax(at),
287 };
288 return Err(StorageError::Envelope {
289 path: last.to_path_buf(),
290 line: at,
291 source,
292 });
293 }
294 let run_prev = check.prev();
295 let run_seq = check.expected();
296
297 let total = bytes.len();
298 let dropped = u64::try_from(total.saturating_sub(valid_len)).unwrap_or(u64::MAX);
299
300 if dropped > 0 {
301 if valid_len == 0 {
302 self.vfs
306 .truncate(last, 0)
307 .map_err(io_err("truncate segment", last))?;
308 self.vfs
309 .sync_data(last)
310 .map_err(io_err("sync segment", last))?;
311 self.vfs
312 .remove_file(last)
313 .map_err(io_err("remove empty segment", last))?;
314 self.vfs
315 .sync_dir(&self.dir.clone())
316 .map_err(io_err("sync ledger dir", &self.dir.clone()))?;
317 match prior {
318 Some(segment) => {
319 self.seg_path = segment.path;
320 self.seg_len = segment.len;
321 }
322 None => {
323 self.seg_path = self.dir.join(segment_file_name(Seq::FIRST));
324 self.seg_len = 0;
325 }
326 }
327 } else {
328 let keep = u64::try_from(valid_len).unwrap_or(u64::MAX);
329 self.vfs
330 .truncate(last, keep)
331 .map_err(io_err("truncate segment", last))?;
332 self.vfs
333 .sync_data(last)
334 .map_err(io_err("sync segment", last))?;
335 self.seg_path = last.to_path_buf();
336 self.seg_len = keep;
337 }
338 } else {
339 self.seg_path = last.to_path_buf();
340 self.seg_len = u64::try_from(total).unwrap_or(u64::MAX);
341 }
342 self.prev = run_prev;
343 self.next_seq = run_seq;
344 Ok((dropped, counted))
345 }
346}
347
348#[derive(Clone, Copy)]
351enum TailProof<'a> {
352 Strict,
354 Records(&'a ProofRecords),
356}
357
358impl TailProof<'_> {
359 fn start(self, last: &Path, bytes: &[u8], entry: LineCheck) -> TailStart {
362 match (self, last.file_name().and_then(|name| name.to_str())) {
363 (TailProof::Records(records), Some(name)) => records.tail_start(name, bytes, entry),
364 (TailProof::Records(_), None) | (TailProof::Strict, _) => {
365 TailStart::strict(bytes, entry)
366 }
367 }
368 }
369}
370
371fn unversioned(v: u64) -> AxError {
376 AxError::failure(
377 AxCode::InvalidArgs,
378 "read a ledger line's version",
379 format!("v{v} is below the first version any build wrote"),
380 )
381 .with_recovery(
382 "restore this segment from its checkpoint commit: the ledger version starts at 1 \
383 and this line declares less",
384 )
385}
386
387#[cfg(test)]
388#[allow(
389 clippy::unwrap_used,
390 clippy::expect_used,
391 clippy::panic,
392 clippy::indexing_slicing,
393 clippy::wildcard_enum_match_arm,
394 clippy::let_underscore_must_use,
395 clippy::let_underscore_untyped,
396 reason = "test code"
397)]
398mod tests;