1use std::io;
17use std::path::{Path, PathBuf};
18
19use kernel::{B3Hash, Seq};
20
21use crate::chain_audit::ProofCount;
22use crate::error::StorageError;
23use crate::jsonl::{JsonlLedger, LineCheck};
24use crate::real_fs::RealFs;
25use crate::vfs::Vfs;
26
27pub(crate) const LINE_CHECK_RULES: &str = "line-check-40e703f48b222ca7";
33
34const MAGIC: &[u8; 8] = b"SPRPRF01";
35const SUFFIX: &str = ".proof";
36const STAGED: &str = ".staged";
37
38pub fn line_check_version() -> u32 {
40 let rules = [env!("CARGO_PKG_VERSION"), LINE_CHECK_RULES].join("\n");
41 let [a, b, c, d, ..] = *B3Hash::digest(rules.as_bytes()).as_bytes();
42 u32::from_le_bytes([a, b, c, d])
43}
44
45#[derive(Debug, Clone)]
47pub struct ProofRecords {
48 dir: PathBuf,
49 may: May,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum May {
56 Read,
57 ReadAndWrite,
58}
59
60impl ProofRecords {
61 pub fn read_only(dir: &Path) -> ProofRecords {
64 ProofRecords {
65 dir: dir.to_path_buf(),
66 may: May::Read,
67 }
68 }
69
70 pub(crate) fn read(&self, segment: &str) -> Option<SegmentRecord> {
75 let path = self.path_of(segment);
76 match RealFs::new().read(&path) {
77 Ok(bytes) => SegmentRecord::decode(&bytes),
78 Err(_unreadable) => None,
79 }
80 }
81
82 pub(crate) fn keep(&self, segment: &str, record: &SegmentRecord) -> Result<(), StorageError> {
88 match self.may {
89 May::Read => Ok(()),
90 May::ReadAndWrite => self.write(segment, record),
91 }
92 }
93
94 fn write(&self, segment: &str, record: &SegmentRecord) -> Result<(), StorageError> {
95 let mut vfs = RealFs::new();
96 let file = self.path_of(segment);
97 let staged = self.dir.join(format!("{segment}{SUFFIX}{STAGED}"));
98 vfs.create_dir_all(&self.dir)
99 .map_err(refused("create the proof records dir", &self.dir))?;
100 if vfs.exists(&staged) {
101 vfs.remove_file(&staged)
102 .map_err(refused("remove a staged proof record", &staged))?;
103 }
104 vfs.append(&staged, &record.encode())
105 .map_err(refused("write a staged proof record", &staged))?;
106 vfs.sync_data(&staged)
107 .map_err(refused("sync a staged proof record", &staged))?;
108 vfs.rename(&staged, &file)
109 .map_err(refused("rename a staged proof record", &file))?;
110 vfs.sync_dir(&self.dir)
111 .map_err(refused("sync the proof records dir", &self.dir))
112 }
113
114 fn path_of(&self, segment: &str) -> PathBuf {
115 self.dir.join(format!("{segment}{SUFFIX}"))
116 }
117}
118
119pub(crate) struct TailStart {
123 pub(crate) from: usize,
124 pub(crate) lines: u64,
125 pub(crate) check: LineCheck,
126 pub(crate) counted: ProofCount,
127}
128
129impl TailStart {
130 pub(crate) fn strict(entry: LineCheck) -> TailStart {
132 TailStart {
133 from: 0,
134 lines: 0,
135 check: entry,
136 counted: ProofCount::default(),
137 }
138 }
139}
140
141impl ProofRecords {
142 pub(crate) fn tail_start(
146 &self,
147 vfs: &dyn Vfs,
148 path: &Path,
149 entry: LineCheck,
150 ) -> io::Result<TailStart> {
151 let mut start = TailStart::strict(entry);
152 let Some(record) = path
153 .file_name()
154 .and_then(|name| name.to_str())
155 .and_then(|name| self.read(name))
156 else {
157 return Ok(start);
158 };
159 let Some(length) = record.prefix_length(vfs.size(path)?, line_check_version()) else {
160 return Ok(start);
161 };
162 if record.entry != entry {
163 return Ok(start);
164 }
165 let mut hasher = blake3::Hasher::new();
166 let mut offset = 0u64;
167 while offset < length {
168 let chunk = vfs.read_at(
169 path,
170 offset,
171 crate::jsonl::SCAN_WINDOW_BYTES.min(length.saturating_sub(offset)),
172 )?;
173 if chunk.is_empty() {
174 return Ok(start);
175 }
176 #[cfg(test)]
177 crate::jsonl::measure_scan(chunk.capacity(), 0);
178 hasher.update(&chunk);
179 let read = u64::try_from(chunk.len()).map_err(io::Error::other)?;
180 offset = offset
181 .checked_add(read)
182 .ok_or_else(|| io::Error::other("prefix offset overflow"))?;
183 start.counted.bytes_read = start.counted.bytes_read.saturating_add(read);
184 start.counted.bytes_hashed = start.counted.bytes_hashed.saturating_add(read);
185 }
186 if let Some((lines, exit)) = record.stands(entry, &hasher) {
187 start.from = usize::try_from(length).map_err(io::Error::other)?;
188 start.lines = lines;
189 start.check = exit;
190 start.counted.segments_by_digest = 1;
191 }
192 Ok(start)
193 }
194}
195
196impl JsonlLedger {
197 pub fn proof_records(&self, dir: &Path) -> ProofRecords {
200 ProofRecords {
201 dir: dir.to_path_buf(),
202 may: match self.lock {
203 Some(_) => May::ReadAndWrite,
204 None => May::Read,
205 },
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
212pub(crate) struct SegmentRecord {
213 pub(crate) version: u32,
214 pub(crate) len: u64,
216 pub(crate) lines: u64,
217 pub(crate) entry: LineCheck,
218 pub(crate) exit: LineCheck,
219 pub(crate) digest: B3Hash,
220}
221
222impl SegmentRecord {
223 fn encode(&self) -> Vec<u8> {
227 let body = [
228 MAGIC.as_slice(),
229 &self.version.to_le_bytes(),
230 &self.len.to_le_bytes(),
231 &self.lines.to_le_bytes(),
232 self.entry.prev().as_bytes(),
233 &self.entry.expected().value().to_le_bytes(),
234 self.exit.prev().as_bytes(),
235 &self.exit.expected().value().to_le_bytes(),
236 self.digest.as_bytes(),
237 ]
238 .concat();
239 let seal = B3Hash::digest(&body);
240 [body.as_slice(), seal.as_bytes()].concat()
241 }
242
243 fn decode(bytes: &[u8]) -> Option<SegmentRecord> {
245 let (body, seal) = bytes.split_last_chunk::<32>()?;
246 if B3Hash::digest(body).as_bytes() != seal {
247 return None;
248 }
249 let (magic, rest) = body.split_first_chunk::<8>()?;
250 if magic != MAGIC {
251 return None;
252 }
253 let (version, rest) = rest.split_first_chunk::<4>()?;
254 let (len, rest) = rest.split_first_chunk::<8>()?;
255 let (lines, rest) = rest.split_first_chunk::<8>()?;
256 let (entry, rest) = chain_state(rest)?;
257 let (exit, rest) = chain_state(rest)?;
258 let digest = <[u8; 32]>::try_from(rest).ok()?;
259 Some(SegmentRecord {
260 version: u32::from_le_bytes(*version),
261 len: u64::from_le_bytes(*len),
262 lines: u64::from_le_bytes(*lines),
263 entry,
264 exit,
265 digest: B3Hash::from_bytes(digest),
266 })
267 }
268}
269
270impl SegmentRecord {
277 pub(crate) fn prefix_length(&self, total: u64, version: u32) -> Option<u64> {
280 (self.version == version && self.len <= total).then_some(self.len)
281 }
282
283 pub(crate) fn prefix<'a>(&self, bytes: &'a [u8], version: u32) -> Option<&'a [u8]> {
287 self.prefix_length(u64::try_from(bytes.len()).ok()?, version)
288 .and_then(|len| usize::try_from(len).ok())
289 .and_then(|len| bytes.get(..len))
290 }
291
292 pub(crate) fn stands(
296 &self,
297 entry: LineCheck,
298 hashed: &blake3::Hasher,
299 ) -> Option<(u64, LineCheck)> {
300 (self.entry == entry && B3Hash::from_bytes(*hashed.finalize().as_bytes()) == self.digest)
301 .then_some((self.lines, self.exit))
302 }
303}
304
305fn chain_state(bytes: &[u8]) -> Option<(LineCheck, &[u8])> {
306 let (prev, rest) = bytes.split_first_chunk::<32>()?;
307 let (expected, rest) = rest.split_first_chunk::<8>()?;
308 Some((
309 LineCheck::after(
310 B3Hash::from_bytes(*prev),
311 Seq::new(u64::from_le_bytes(*expected)),
312 ),
313 rest,
314 ))
315}
316
317fn refused(op: &'static str, path: &Path) -> impl FnOnce(io::Error) -> StorageError {
320 let path = path.to_path_buf();
321 move |source| StorageError::Snapshot { op, path, source }
322}