Skip to main content

storage/jsonl/
tail.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//! The ledger read from its tail: the newest line first, segment by
7//! segment backwards, each segment read from its end through a window
8//! that starts at one page and doubles (the rule `first_line` uses), so
9//! a reader that wants the last N records pays for their bytes and not
10//! for the whole ledger (`crates/storage/spec/Jsonl.lean` ยง8-1). Each line passes the same
11//! judgement a forward reader applies (`LineCheck::judge`); the chain is
12//! linked from the newer end instead.
13
14use std::io;
15use std::path::{Path, PathBuf};
16
17use kernel::ledger::chain_hash;
18use kernel::{B3Hash, GENESIS_PREV, Seq};
19
20use crate::error::{StorageError, io_err};
21use crate::real_fs::RealFs;
22use crate::vfs::Vfs;
23
24use super::ledger::is_segment;
25use super::verify::{CheckedLine, LineCheck, LineFault};
26
27const FIRST_WINDOW_BYTES: u64 = 4096;
28
29/// One line of the ledger, newest first: its bytes and what it is.
30#[derive(Debug, Clone, PartialEq)]
31pub struct TailLine {
32    pub raw: Vec<u8>,
33    pub checked: CheckedLine,
34}
35
36/// The ledger's lines from the newest back. The walk ends after the
37/// oldest line, or after the first failure it yields.
38pub struct TailLines {
39    vfs: Box<dyn Vfs>,
40    segments: Vec<PathBuf>,
41    reading: Option<Backward>,
42    newer: Option<Link>,
43    ended: bool,
44}
45
46/// What the line read before (the newer one) requires of the next.
47#[derive(Clone, Copy)]
48struct Link {
49    seq: Seq,
50    prev: B3Hash,
51}
52
53/// One segment being read from its end. `held` is the segment's bytes
54/// from `unread` up to the end of its last complete line.
55struct Backward {
56    path: PathBuf,
57    unread: u64,
58    window: u64,
59    held: Vec<u8>,
60}
61
62impl TailLines {
63    /// The walk over the ledger in `dir`, read only: it never opens the
64    /// ledger, never repairs and never writes.
65    ///
66    /// # Errors
67    /// The directory cannot be listed.
68    pub fn at(dir: &Path) -> Result<Self, StorageError> {
69        Self::through(Box::new(RealFs::new()), dir)
70    }
71
72    pub(crate) fn through(vfs: Box<dyn Vfs>, dir: &Path) -> Result<Self, StorageError> {
73        let segments = vfs
74            .list(dir)
75            .map_err(io_err("list ledger dir", dir))?
76            .into_iter()
77            .filter(|p| is_segment(p))
78            .collect();
79        Ok(Self {
80            vfs,
81            segments,
82            reading: None,
83            newer: None,
84            ended: false,
85        })
86    }
87
88    /// The next non-empty line back, crossing into older segments.
89    fn next_raw(&mut self) -> Result<Option<Vec<u8>>, StorageError> {
90        loop {
91            let reading = match self.reading.as_mut() {
92                Some(reading) => reading,
93                None => match self.segments.pop() {
94                    Some(path) => self
95                        .reading
96                        .insert(Backward::open(self.vfs.as_ref(), path)?),
97                    None => return Ok(None),
98                },
99            };
100            match reading
101                .pop_line(self.vfs.as_ref())
102                .map_err(io_err("read segment", &reading.path))?
103            {
104                Some(line) if line.is_empty() => {}
105                Some(line) => return Ok(Some(line)),
106                None => self.reading = None,
107            }
108        }
109    }
110
111    /// Judges `raw` as a forward reader would and links it to the newer
112    /// line: its hash is the newer line's `prev`, its seq the one before.
113    fn link(&self, raw: &[u8]) -> Result<(Link, CheckedLine), LineFault> {
114        let judged = LineCheck::judge(raw)?;
115        if let Some(newer) = self.newer {
116            if chain_hash(raw) != newer.prev {
117                return Err(LineFault::ChainBreak);
118            }
119            if judged.seq.next().map_err(LineFault::SeqExhausted)? != newer.seq {
120                return Err(LineFault::SeqGap {
121                    found: judged.seq,
122                    expected: Seq::new(newer.seq.value().saturating_sub(1)),
123                });
124            }
125        }
126        if judged.seq == Seq::FIRST && judged.prev != GENESIS_PREV {
127            return Err(LineFault::ChainBreak);
128        }
129        let link = Link {
130            seq: judged.seq,
131            prev: judged.prev,
132        };
133        Ok((link, judged.checked))
134    }
135
136    /// A refused line, named by the 1-based line the chain places it
137    /// at: the newer line's seq. The newest line has no newer one, and
138    /// is named line 0.
139    fn refusal(&self, fault: LineFault) -> StorageError {
140        let line = self.newer.map_or(0, |newer| newer.seq.value());
141        StorageError::Envelope {
142            path: self
143                .reading
144                .as_ref()
145                .map(|reading| reading.path.clone())
146                .unwrap_or_default(),
147            line,
148            source: fault.into_ax(line),
149        }
150    }
151}
152
153impl Iterator for TailLines {
154    type Item = Result<TailLine, StorageError>;
155
156    fn next(&mut self) -> Option<Self::Item> {
157        if self.ended {
158            return None;
159        }
160        let raw = match self.next_raw() {
161            Ok(Some(raw)) => raw,
162            Ok(None) => return None,
163            Err(e) => {
164                self.ended = true;
165                return Some(Err(e));
166            }
167        };
168        match self.link(&raw) {
169            Ok((link, checked)) => {
170                self.newer = Some(link);
171                Some(Ok(TailLine { raw, checked }))
172            }
173            Err(fault) => {
174                self.ended = true;
175                Some(Err(self.refusal(fault)))
176            }
177        }
178    }
179}
180impl Backward {
181    /// The segment at `path`, its torn tail (the bytes after its last
182    /// `\n`) already set aside: they are not a line.
183    fn open(vfs: &dyn Vfs, path: PathBuf) -> Result<Self, StorageError> {
184        let unread = vfs.size(&path).map_err(io_err("size segment", &path))?;
185        let mut backward = Self {
186            path,
187            unread,
188            window: FIRST_WINDOW_BYTES,
189            held: Vec::new(),
190        };
191        loop {
192            if let Some(end) = backward.held.iter().rposition(|byte| *byte == b'\n') {
193                backward.held.truncate(end.saturating_add(1));
194                return Ok(backward);
195            }
196            if backward.unread == 0 {
197                backward.held.clear();
198                return Ok(backward);
199            }
200            backward
201                .read_further(vfs)
202                .map_err(io_err("read segment", &backward.path))?;
203        }
204    }
205
206    /// The last line `held` ends with, without its `\n`; `None` once the
207    /// segment has no line left.
208    fn pop_line(&mut self, vfs: &dyn Vfs) -> io::Result<Option<Vec<u8>>> {
209        loop {
210            let Some((_newline, body)) = self.held.split_last() else {
211                return Ok(None);
212            };
213            let start = match body.iter().rposition(|byte| *byte == b'\n') {
214                Some(newline) => newline.saturating_add(1),
215                None if self.unread == 0 => 0,
216                None => {
217                    self.read_further(vfs)?;
218                    continue;
219                }
220            };
221            let line = body.get(start..).map(<[u8]>::to_vec);
222            self.held.truncate(start);
223            return Ok(line);
224        }
225    }
226
227    /// Puts the window of bytes before `held` in front of it, and doubles
228    /// the window for the next read.
229    fn read_further(&mut self, vfs: &dyn Vfs) -> io::Result<()> {
230        let take = self.window.min(self.unread);
231        let from = self.unread.saturating_sub(take);
232        let mut bytes = vfs.read_at(&self.path, from, take)?;
233        bytes.append(&mut self.held);
234        self.held = bytes;
235        self.unread = from;
236        self.window = self.window.saturating_mul(2);
237        Ok(())
238    }
239}
240
241#[cfg(test)]
242#[allow(
243    clippy::unwrap_used,
244    clippy::expect_used,
245    clippy::panic,
246    clippy::indexing_slicing,
247    clippy::as_conversions,
248    clippy::arithmetic_side_effects,
249    reason = "test code"
250)]
251mod tests;