1use std::path::Path;
18
19use kernel::{Address, AxCode, AxError, B3Hash, EventRecord, EventRef, Seq};
20use storage::{CheckedLine, LedgerIndex, LineCheck, Located};
21
22#[derive(Debug, Clone, PartialEq)]
25pub enum VerifiedLine {
26 Known { record: EventRecord, echo: EventRef },
27 IgnoredUnknown { seq: Seq },
28}
29
30#[derive(Debug)]
34pub struct VerifiedLedger {
35 raw: Vec<Vec<u8>>,
36 verified: Vec<VerifiedLine>,
37}
38
39impl VerifiedLedger {
40 pub fn raw_lines(&self) -> &[Vec<u8>] {
41 &self.raw
42 }
43
44 pub fn lines(&self) -> &[VerifiedLine] {
45 &self.verified
46 }
47
48 pub fn tail_seq(&self) -> Option<Seq> {
49 let count = u64::try_from(self.verified.len()).unwrap_or(u64::MAX);
50 count.checked_sub(1).map(Seq::new)
51 }
52}
53
54pub fn verify_lines(lines: Vec<Vec<u8>>) -> Result<VerifiedLedger, AxError> {
57 let mut check = LineCheck::at_genesis();
58 let verified = lines
59 .iter()
60 .zip(1u64..)
61 .map(|(raw, line_no)| {
62 check
63 .advance(raw)
64 .map(|checked| match checked {
65 CheckedLine::Known(record) => VerifiedLine::Known {
66 echo: record.to_ref(),
67 record,
68 },
69 CheckedLine::IgnoredUnknown(seq) => VerifiedLine::IgnoredUnknown { seq },
70 })
71 .map_err(|fault| fault.into_ax(line_no))
72 })
73 .collect::<Result<Vec<_>, _>>()?;
74 Ok(VerifiedLedger {
75 raw: lines,
76 verified,
77 })
78}
79
80pub fn verify_ledger_dir(dir: &Path) -> Result<VerifiedLedger, AxError> {
90 let lines = storage::read_raw_lines_at(dir).map_err(storage::StorageError::into_ax)?;
91 verify_lines(lines)
92}
93
94pub fn fold_ledger_dir(
106 dir: &Path,
107 mut each: impl FnMut(&EventRecord) -> Result<(), AxError>,
108) -> Result<LedgerIndex, AxError> {
109 let mut check = LineCheck::at_genesis();
110 let mut line_no = 0u64;
111 LedgerIndex::folding(dir, |raw| {
112 line_no = line_no.saturating_add(1);
113 match check.advance(raw).map_err(|fault| fault.into_ax(line_no))? {
114 CheckedLine::Known(record) => {
115 each(&record)?;
116 Ok(Some(Located {
117 seq: record.seq(),
118 run: Some(record.run()),
119 }))
120 }
121 CheckedLine::IgnoredUnknown(_) => Ok(None),
122 }
123 })
124}
125
126pub fn rebuild_prefix(
131 data: &serde_json::Value,
132 resolver: &dyn Fn(&Address) -> Option<Vec<u8>>,
133) -> Result<[B3Hash; 4], AxError> {
134 let segments = data
135 .get("segments")
136 .and_then(serde_json::Value::as_array)
137 .ok_or_else(|| {
138 AxError::failure(
139 AxCode::InvalidArgs,
140 "rebuild prefix",
141 "payload has no segments",
142 )
143 .with_recovery(
144 "replay a run whose `prompt_assembled` line carries `segments`; a \
145 hand-written line cannot be rebuilt",
146 )
147 })?;
148 if segments.len() != 4 {
149 return Err(AxError::failure(
150 AxCode::InvalidArgs,
151 "rebuild prefix",
152 format!("expected 4 segments, found {}", segments.len()),
153 )
154 .with_recovery(
155 "replay a run recorded by this build: a prefix is four segments, city, \
156 building, resident and run",
157 ));
158 }
159 let mut hashes = Vec::with_capacity(4);
160 for segment in segments {
161 let slot = segment
162 .get("slot")
163 .and_then(serde_json::Value::as_str)
164 .unwrap_or("?");
165 let sources = segment
166 .get("sources")
167 .and_then(serde_json::Value::as_array)
168 .ok_or_else(|| {
169 AxError::failure(
170 AxCode::InvalidArgs,
171 "rebuild prefix",
172 format!("{slot}: payload lacks source notes (hand-assembled prefix?)"),
173 )
174 .with_recovery(format!(
175 "replay a run whose {slot} segment lists the documents it was \
176 built from; a prefix assembled by hand names none"
177 ))
178 })?;
179 let mut text = String::new();
180 for source in sources {
181 let addr_raw = source
182 .get("addr")
183 .and_then(serde_json::Value::as_str)
184 .ok_or_else(|| {
185 AxError::failure(AxCode::InvalidArgs, "rebuild prefix", "source without addr")
186 .with_recovery(
187 "replay a run whose every source note carries `addr`; \
188 without it the document cannot be found again",
189 )
190 })?;
191 let addr = Address::parse(addr_raw)?;
192 let bytes = resolver(&addr).ok_or_else(|| {
193 AxError::failure(AxCode::PathNotFound, "rebuild prefix", addr_raw)
194 .with_recovery("supply the source document at its recorded address")
195 })?;
196 let body = std::str::from_utf8(&bytes).map_err(|_| {
197 AxError::failure(
198 AxCode::InvalidArgs,
199 "rebuild prefix",
200 format!("{addr_raw}: not utf-8"),
201 )
202 .with_recovery(format!(
203 "restore {addr_raw} to the UTF-8 text it held when the run \
204 recorded it; the bytes on disk today are something else"
205 ))
206 })?;
207 let kept = usize::try_from(
208 source
209 .get("kept")
210 .and_then(serde_json::Value::as_u64)
211 .unwrap_or(0),
212 )
213 .map_err(|_| {
214 AxError::failure(AxCode::InvalidArgs, "rebuild prefix", "kept exceeds usize")
215 .with_recovery(
216 "replay this run on a 64-bit machine: the recorded kept span \
217 is longer than this one can address",
218 )
219 })?;
220 let marker = source
221 .get("marker")
222 .and_then(serde_json::Value::as_bool)
223 .unwrap_or(false);
224 let dropped = source
225 .get("dropped")
226 .and_then(serde_json::Value::as_u64)
227 .unwrap_or(0);
228 let piece = body.get(..kept).ok_or_else(|| {
229 AxError::failure(
230 AxCode::InvalidArgs,
231 "rebuild prefix",
232 format!("{addr_raw}: kept span exceeds the document"),
233 )
234 .with_recovery(format!(
235 "restore {addr_raw} to the text it held when the run recorded it; \
236 the file on disk today is shorter than the span the run kept"
237 ))
238 })?;
239 if !text.is_empty() {
240 text.push_str(crate::prefix::DOC_JOIN);
241 }
242 text.push_str(piece);
243 if marker {
244 text.push_str(&crate::elision::marker(kernel::ByteLen::new(dropped)));
245 }
246 }
247 let rebuilt = B3Hash::digest(text.as_bytes());
248 if let Some(recorded) = segment.get("hash").and_then(serde_json::Value::as_str)
249 && recorded != rebuilt.to_string()
250 {
251 return Err(AxError::failure(
252 AxCode::CasCorrupt,
253 "rebuild prefix",
254 format!("{slot}: rebuilt hash differs from the recorded one"),
255 )
256 .with_recovery("the source documents no longer match the recorded assembly"));
257 }
258 hashes.push(rebuilt);
259 }
260 let four: [B3Hash; 4] = match hashes.try_into() {
261 Ok(array) => array,
262 Err(_) => {
263 return Err(AxError::failure(
264 AxCode::InvalidArgs,
265 "rebuild prefix",
266 "segment count drifted during rebuild",
267 )
268 .with_recovery(
269 "report this against runtime::replay: four segments went into the \
270 rebuild and a different number came out",
271 ));
272 }
273 };
274 Ok(four)
275}
276
277mod resume;
280
281pub use resume::{DanglingCalls, outcome_unknown_draft};
282
283mod shape;
286
287pub use shape::rebuild_shape;
288
289#[cfg(test)]
290#[allow(
291 clippy::unwrap_used,
292 clippy::expect_used,
293 clippy::panic,
294 clippy::indexing_slicing,
295 reason = "test code"
296)]
297mod tests;