1use std::collections::HashMap;
16use std::fs::File;
17use std::io::{self, BufReader, Read};
18use std::path::Path;
19
20use thiserror::Error;
21
22use crate::proof::{Proof, ProofNodeId, ProofStep};
23
24const MAGIC: &[u8; 8] = b"OXIZPROF";
26const FORMAT_VERSION: u32 = 1;
27const KIND_AXIOM: u8 = 0;
28const KIND_INFERENCE: u8 = 1;
29
30#[derive(Error, Debug)]
32pub enum ProofError {
33 #[error("I/O error: {0}")]
35 Io(#[from] io::Error),
36
37 #[error("invalid file magic; expected OXIZPROF, got {found:?}")]
39 InvalidMagic {
40 found: [u8; 8],
42 },
43
44 #[error(
46 "unsupported format version {version}; this reader supports version {}",
47 FORMAT_VERSION
48 )]
49 UnsupportedVersion {
50 version: u32,
52 },
53
54 #[error("unknown record kind {0:#x}")]
56 UnknownKind(u8),
57
58 #[error(
60 "forward reference: node {referencing} references premise {missing} which has not been defined"
61 )]
62 ForwardReference {
63 referencing: u32,
65 missing: u32,
67 },
68
69 #[error("invalid UTF-8 in proof step: {0}")]
71 InvalidUtf8(String),
72
73 #[error("proof log is empty or truncated after the header")]
75 EmptyLog,
76
77 #[error("field length {0} exceeds the safety limit")]
79 FieldTooLarge(u32),
80}
81
82pub type ReplayResult<T> = Result<T, ProofError>;
84
85const MAX_FIELD_BYTES: u32 = 64 * 1024 * 1024;
87
88const MAX_PREMISES: u32 = 1_000_000;
90
91const MAX_ARGS: u32 = 1_000;
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum VerificationResult {
97 Valid,
99 Invalid(String),
101 Incomplete,
104}
105
106impl VerificationResult {
107 #[must_use]
109 pub fn is_valid(&self) -> bool {
110 matches!(self, Self::Valid)
111 }
112
113 #[must_use]
115 pub fn invalid_reason(&self) -> Option<&str> {
116 match self {
117 Self::Invalid(reason) => Some(reason.as_str()),
118 _ => None,
119 }
120 }
121}
122
123impl std::fmt::Display for VerificationResult {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 match self {
126 Self::Valid => write!(f, "valid"),
127 Self::Invalid(reason) => write!(f, "invalid: {}", reason),
128 Self::Incomplete => write!(f, "incomplete"),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct ReplayStats {
136 pub total_steps: u64,
138 pub axiom_count: u64,
140 pub inference_count: u64,
142 pub total_premises: u64,
144}
145
146pub struct ProofReplayer {
164 proof: Proof,
166 id_map: HashMap<u32, ProofNodeId>,
168 stats: ReplayStats,
170}
171
172impl ProofReplayer {
173 #[must_use]
175 pub fn new() -> Self {
176 Self {
177 proof: Proof::new(),
178 id_map: HashMap::new(),
179 stats: ReplayStats::default(),
180 }
181 }
182
183 pub fn replay_from_file(path: &Path) -> ReplayResult<VerificationResult> {
193 let mut replayer = Self::new();
194 replayer.replay(path)
195 }
196
197 pub fn replay(&mut self, path: &Path) -> ReplayResult<VerificationResult> {
201 let file = File::open(path)?;
202 let mut reader = BufReader::new(file);
203
204 read_header(&mut reader)?;
206
207 let mut steps_read = 0u64;
209 loop {
210 match read_record(&mut reader) {
211 Ok(Some(record)) => {
212 self.incorporate_record(record)?;
213 steps_read += 1;
214 }
215 Ok(None) => break, Err(ProofError::Io(ref e)) if e.kind() == io::ErrorKind::UnexpectedEof => {
217 if steps_read == 0 {
218 return Err(ProofError::EmptyLog);
219 }
220 return Ok(VerificationResult::Incomplete);
222 }
223 Err(e) => return Err(e),
224 }
225 }
226
227 if steps_read == 0 {
228 return Err(ProofError::EmptyLog);
229 }
230
231 Ok(self.assess())
233 }
234
235 #[must_use]
237 pub fn into_proof(self) -> Proof {
238 self.proof
239 }
240
241 #[must_use]
243 pub fn proof(&self) -> &Proof {
244 &self.proof
245 }
246
247 #[must_use]
249 pub fn stats(&self) -> &ReplayStats {
250 &self.stats
251 }
252
253 fn incorporate_record(&mut self, record: LogRecord) -> ReplayResult<()> {
257 let log_id = record.log_id;
258
259 let mem_id = match record.step {
260 ProofStep::Axiom { ref conclusion } => {
261 self.stats.axiom_count += 1;
262 self.proof.add_axiom(conclusion.clone())
263 }
264 ProofStep::Inference {
265 ref rule,
266 ref premises,
267 ref conclusion,
268 ref args,
269 } => {
270 self.stats.inference_count += 1;
271 self.stats.total_premises += premises.len() as u64;
272
273 let mut mem_premises = Vec::with_capacity(premises.len());
276 for p_node_id in premises.iter() {
277 let p_log_id = p_node_id.0;
278 let mem_premise = self.id_map.get(&p_log_id).copied().ok_or(
279 ProofError::ForwardReference {
280 referencing: log_id,
281 missing: p_log_id,
282 },
283 )?;
284 mem_premises.push(mem_premise);
285 }
286
287 self.proof.add_inference_with_args(
288 rule.clone(),
289 mem_premises,
290 args.iter().cloned().collect(),
291 conclusion.clone(),
292 )
293 }
294 };
295
296 self.stats.total_steps += 1;
297 self.id_map.insert(log_id, mem_id);
298 Ok(())
299 }
300
301 fn assess(&self) -> VerificationResult {
303 let stats = self.proof.stats();
304 if stats.total_nodes == 0 {
305 return VerificationResult::Incomplete;
306 }
307 VerificationResult::Valid
309 }
310}
311
312impl Default for ProofReplayer {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318struct LogRecord {
322 log_id: u32,
324 step: ProofStep,
326}
327
328fn read_header<R: Read>(r: &mut R) -> ReplayResult<()> {
330 let mut magic = [0u8; 8];
331 r.read_exact(&mut magic)?;
332 if &magic != MAGIC {
333 return Err(ProofError::InvalidMagic { found: magic });
334 }
335
336 let version = read_u32(r)?;
337 if version != FORMAT_VERSION {
338 return Err(ProofError::UnsupportedVersion { version });
339 }
340
341 let _flags = read_u32(r)?;
343
344 let mut reserved = [0u8; 16];
346 r.read_exact(&mut reserved)?;
347
348 Ok(())
349}
350
351fn read_record<R: Read>(r: &mut R) -> ReplayResult<Option<LogRecord>> {
353 let mut kind_buf = [0u8; 1];
355 match r.read(&mut kind_buf) {
356 Ok(0) => return Ok(None),
357 Ok(_) => {}
358 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
359 Err(e) => return Err(ProofError::Io(e)),
360 }
361 let kind = kind_buf[0];
362
363 let log_id = read_u32(r)?;
364 let conclusion = read_string(r)?;
365
366 let step = match kind {
367 KIND_AXIOM => ProofStep::Axiom { conclusion },
368 KIND_INFERENCE => {
369 let rule = read_string(r)?;
370
371 let premise_count = read_u32(r)?;
372 if premise_count > MAX_PREMISES {
373 return Err(ProofError::FieldTooLarge(premise_count));
374 }
375 let mut premises = smallvec::SmallVec::<[ProofNodeId; 4]>::new();
376 for _ in 0..premise_count {
377 premises.push(ProofNodeId(read_u32(r)?));
378 }
379
380 let arg_count = read_u32(r)?;
381 if arg_count > MAX_ARGS {
382 return Err(ProofError::FieldTooLarge(arg_count));
383 }
384 let mut args = smallvec::SmallVec::<[String; 2]>::new();
385 for _ in 0..arg_count {
386 args.push(read_string(r)?);
387 }
388
389 ProofStep::Inference {
390 rule,
391 premises,
392 conclusion,
393 args,
394 }
395 }
396 other => return Err(ProofError::UnknownKind(other)),
397 };
398
399 Ok(Some(LogRecord { log_id, step }))
400}
401
402fn read_u32<R: Read>(r: &mut R) -> ReplayResult<u32> {
404 let mut buf = [0u8; 4];
405 r.read_exact(&mut buf)?;
406 Ok(u32::from_le_bytes(buf))
407}
408
409fn read_string<R: Read>(r: &mut R) -> ReplayResult<String> {
411 let len = read_u32(r)?;
412 if len > MAX_FIELD_BYTES {
413 return Err(ProofError::FieldTooLarge(len));
414 }
415 let mut buf = vec![0u8; len as usize];
416 r.read_exact(&mut buf)?;
417 String::from_utf8(buf).map_err(|e| ProofError::InvalidUtf8(e.to_string()))
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::logging::ProofLogger;
424 use crate::proof::{ProofNodeId, ProofStep};
425 use smallvec::SmallVec;
426
427 fn temp_path(name: &str) -> std::path::PathBuf {
428 std::env::temp_dir().join(format!("oxiz_replay_{}.oxizlog", name))
429 }
430
431 #[test]
432 fn test_roundtrip_axiom() {
433 let path = temp_path("roundtrip_axiom");
434 let _ = std::fs::remove_file(&path);
435
436 {
437 let mut logger = ProofLogger::create(&path).expect("create");
438 let step = ProofStep::Axiom {
439 conclusion: "(= x 0)".to_string(),
440 };
441 logger.log_step(ProofNodeId(0), &step).expect("log");
442 logger.close().expect("close");
443 }
444
445 let result = ProofReplayer::replay_from_file(&path).expect("replay");
446 assert_eq!(result, VerificationResult::Valid);
447
448 let _ = std::fs::remove_file(&path);
449 }
450
451 #[test]
452 fn test_roundtrip_inference_chain() {
453 let path = temp_path("roundtrip_chain");
454 let _ = std::fs::remove_file(&path);
455
456 {
457 let mut logger = ProofLogger::create(&path).expect("create");
458
459 logger
461 .log_step(
462 ProofNodeId(0),
463 &ProofStep::Axiom {
464 conclusion: "p".to_string(),
465 },
466 )
467 .expect("log");
468
469 logger
471 .log_step(
472 ProofNodeId(1),
473 &ProofStep::Axiom {
474 conclusion: "(=> p q)".to_string(),
475 },
476 )
477 .expect("log");
478
479 logger
481 .log_step(
482 ProofNodeId(2),
483 &ProofStep::Inference {
484 rule: "mp".to_string(),
485 premises: SmallVec::from_vec(vec![ProofNodeId(0), ProofNodeId(1)]),
486 conclusion: "q".to_string(),
487 args: SmallVec::new(),
488 },
489 )
490 .expect("log");
491
492 logger.close().expect("close");
493 }
494
495 let mut replayer = ProofReplayer::new();
496 let result = replayer.replay(&path).expect("replay");
497 assert_eq!(result, VerificationResult::Valid);
498 assert_eq!(replayer.stats().total_steps, 3);
499 assert_eq!(replayer.stats().axiom_count, 2);
500 assert_eq!(replayer.stats().inference_count, 1);
501 assert_eq!(replayer.stats().total_premises, 2);
502
503 let _ = std::fs::remove_file(&path);
504 }
505
506 #[test]
507 fn test_empty_log_is_error() {
508 let path = temp_path("empty_log");
509 let _ = std::fs::remove_file(&path);
510
511 {
513 let mut logger = ProofLogger::create(&path).expect("create");
514 logger.close().expect("close");
515 }
516
517 let result = ProofReplayer::replay_from_file(&path);
518 assert!(matches!(result, Err(ProofError::EmptyLog)));
519
520 let _ = std::fs::remove_file(&path);
521 }
522
523 #[test]
524 fn test_bad_magic() {
525 let path = temp_path("bad_magic");
526 let _ = std::fs::remove_file(&path);
527
528 std::fs::write(
529 &path,
530 b"BADMAGIC\x01\x00\x00\x00\x00\x00\x00\x00\
531 \x00\x00\x00\x00\x00\x00\x00\x00\
532 \x00\x00\x00\x00\x00\x00\x00\x00",
533 )
534 .expect("write");
535
536 let err = ProofReplayer::replay_from_file(&path).expect_err("should fail");
537 assert!(matches!(err, ProofError::InvalidMagic { .. }));
538
539 let _ = std::fs::remove_file(&path);
540 }
541
542 #[test]
543 fn test_verification_result_display() {
544 assert_eq!(VerificationResult::Valid.to_string(), "valid");
545 assert_eq!(
546 VerificationResult::Invalid("bad step".to_string()).to_string(),
547 "invalid: bad step"
548 );
549 assert_eq!(VerificationResult::Incomplete.to_string(), "incomplete");
550 }
551}