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