use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufReader, Read};
use std::path::Path;
use thiserror::Error;
use crate::proof::{Proof, ProofNodeId, ProofStep};
const MAGIC: &[u8; 8] = b"OXIZPROF";
const FORMAT_VERSION: u32 = 1;
const KIND_AXIOM: u8 = 0;
const KIND_INFERENCE: u8 = 1;
#[derive(Error, Debug)]
pub enum ProofError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("invalid file magic; expected OXIZPROF, got {found:?}")]
InvalidMagic {
found: [u8; 8],
},
#[error(
"unsupported format version {version}; this reader supports version {}",
FORMAT_VERSION
)]
UnsupportedVersion {
version: u32,
},
#[error("unknown record kind {0:#x}")]
UnknownKind(u8),
#[error(
"forward reference: node {referencing} references premise {missing} which has not been defined"
)]
ForwardReference {
referencing: u32,
missing: u32,
},
#[error("invalid UTF-8 in proof step: {0}")]
InvalidUtf8(String),
#[error("proof log is empty or truncated after the header")]
EmptyLog,
#[error("field length {0} exceeds the safety limit")]
FieldTooLarge(u32),
}
pub type ReplayResult<T> = Result<T, ProofError>;
const MAX_FIELD_BYTES: u32 = 64 * 1024 * 1024;
const MAX_PREMISES: u32 = 1_000_000;
const MAX_ARGS: u32 = 1_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerificationResult {
Valid,
Invalid(String),
Incomplete,
}
impl VerificationResult {
#[must_use]
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
#[must_use]
pub fn invalid_reason(&self) -> Option<&str> {
match self {
Self::Invalid(reason) => Some(reason.as_str()),
_ => None,
}
}
}
impl std::fmt::Display for VerificationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Valid => write!(f, "valid"),
Self::Invalid(reason) => write!(f, "invalid: {}", reason),
Self::Incomplete => write!(f, "incomplete"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ReplayStats {
pub total_steps: u64,
pub axiom_count: u64,
pub inference_count: u64,
pub total_premises: u64,
}
pub struct ProofReplayer {
proof: Proof,
id_map: HashMap<u32, ProofNodeId>,
stats: ReplayStats,
}
impl ProofReplayer {
#[must_use]
pub fn new() -> Self {
Self {
proof: Proof::new(),
id_map: HashMap::new(),
stats: ReplayStats::default(),
}
}
pub fn replay_from_file(path: &Path) -> ReplayResult<VerificationResult> {
let mut replayer = Self::new();
replayer.replay(path)
}
pub fn replay(&mut self, path: &Path) -> ReplayResult<VerificationResult> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
read_header(&mut reader)?;
let mut steps_read = 0u64;
loop {
match read_record(&mut reader) {
Ok(Some(record)) => {
self.incorporate_record(record)?;
steps_read += 1;
}
Ok(None) => break, Err(ProofError::Io(ref e)) if e.kind() == io::ErrorKind::UnexpectedEof => {
if steps_read == 0 {
return Err(ProofError::EmptyLog);
}
return Ok(VerificationResult::Incomplete);
}
Err(e) => return Err(e),
}
}
if steps_read == 0 {
return Err(ProofError::EmptyLog);
}
Ok(self.assess())
}
#[must_use]
pub fn into_proof(self) -> Proof {
self.proof
}
#[must_use]
pub fn proof(&self) -> &Proof {
&self.proof
}
#[must_use]
pub fn stats(&self) -> &ReplayStats {
&self.stats
}
fn incorporate_record(&mut self, record: LogRecord) -> ReplayResult<()> {
let log_id = record.log_id;
let mem_id = match record.step {
ProofStep::Axiom { ref conclusion } => {
self.stats.axiom_count += 1;
self.proof.add_axiom(conclusion.clone())
}
ProofStep::Inference {
ref rule,
ref premises,
ref conclusion,
ref args,
} => {
self.stats.inference_count += 1;
self.stats.total_premises += premises.len() as u64;
let mut mem_premises = Vec::with_capacity(premises.len());
for p_node_id in premises.iter() {
let p_log_id = p_node_id.0;
let mem_premise = self.id_map.get(&p_log_id).copied().ok_or(
ProofError::ForwardReference {
referencing: log_id,
missing: p_log_id,
},
)?;
mem_premises.push(mem_premise);
}
self.proof.add_inference_with_args(
rule.clone(),
mem_premises,
args.iter().cloned().collect(),
conclusion.clone(),
)
}
};
self.stats.total_steps += 1;
self.id_map.insert(log_id, mem_id);
Ok(())
}
fn assess(&self) -> VerificationResult {
let stats = self.proof.stats();
if stats.total_nodes == 0 {
return VerificationResult::Incomplete;
}
VerificationResult::Valid
}
}
impl Default for ProofReplayer {
fn default() -> Self {
Self::new()
}
}
struct LogRecord {
log_id: u32,
step: ProofStep,
}
fn read_header<R: Read>(r: &mut R) -> ReplayResult<()> {
let mut magic = [0u8; 8];
r.read_exact(&mut magic)?;
if &magic != MAGIC {
return Err(ProofError::InvalidMagic { found: magic });
}
let version = read_u32(r)?;
if version != FORMAT_VERSION {
return Err(ProofError::UnsupportedVersion { version });
}
let _flags = read_u32(r)?;
let mut reserved = [0u8; 16];
r.read_exact(&mut reserved)?;
Ok(())
}
fn read_record<R: Read>(r: &mut R) -> ReplayResult<Option<LogRecord>> {
let mut kind_buf = [0u8; 1];
match r.read(&mut kind_buf) {
Ok(0) => return Ok(None),
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(ProofError::Io(e)),
}
let kind = kind_buf[0];
let log_id = read_u32(r)?;
let conclusion = read_string(r)?;
let step = match kind {
KIND_AXIOM => ProofStep::Axiom { conclusion },
KIND_INFERENCE => {
let rule = read_string(r)?;
let premise_count = read_u32(r)?;
if premise_count > MAX_PREMISES {
return Err(ProofError::FieldTooLarge(premise_count));
}
let mut premises = smallvec::SmallVec::<[ProofNodeId; 4]>::new();
for _ in 0..premise_count {
premises.push(ProofNodeId(read_u32(r)?));
}
let arg_count = read_u32(r)?;
if arg_count > MAX_ARGS {
return Err(ProofError::FieldTooLarge(arg_count));
}
let mut args = smallvec::SmallVec::<[String; 2]>::new();
for _ in 0..arg_count {
args.push(read_string(r)?);
}
ProofStep::Inference {
rule,
premises,
conclusion,
args,
}
}
other => return Err(ProofError::UnknownKind(other)),
};
Ok(Some(LogRecord { log_id, step }))
}
fn read_u32<R: Read>(r: &mut R) -> ReplayResult<u32> {
let mut buf = [0u8; 4];
r.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_string<R: Read>(r: &mut R) -> ReplayResult<String> {
let len = read_u32(r)?;
if len > MAX_FIELD_BYTES {
return Err(ProofError::FieldTooLarge(len));
}
let mut buf = vec![0u8; len as usize];
r.read_exact(&mut buf)?;
String::from_utf8(buf).map_err(|e| ProofError::InvalidUtf8(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::logging::ProofLogger;
use crate::proof::{ProofNodeId, ProofStep};
use smallvec::SmallVec;
fn temp_path(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("oxiz_replay_{}.oxizlog", name))
}
#[test]
fn test_roundtrip_axiom() {
let path = temp_path("roundtrip_axiom");
let _ = std::fs::remove_file(&path);
{
let mut logger = ProofLogger::create(&path).expect("create");
let step = ProofStep::Axiom {
conclusion: "(= x 0)".to_string(),
};
logger.log_step(ProofNodeId(0), &step).expect("log");
logger.close().expect("close");
}
let result = ProofReplayer::replay_from_file(&path).expect("replay");
assert_eq!(result, VerificationResult::Valid);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_roundtrip_inference_chain() {
let path = temp_path("roundtrip_chain");
let _ = std::fs::remove_file(&path);
{
let mut logger = ProofLogger::create(&path).expect("create");
logger
.log_step(
ProofNodeId(0),
&ProofStep::Axiom {
conclusion: "p".to_string(),
},
)
.expect("log");
logger
.log_step(
ProofNodeId(1),
&ProofStep::Axiom {
conclusion: "(=> p q)".to_string(),
},
)
.expect("log");
logger
.log_step(
ProofNodeId(2),
&ProofStep::Inference {
rule: "mp".to_string(),
premises: SmallVec::from_vec(vec![ProofNodeId(0), ProofNodeId(1)]),
conclusion: "q".to_string(),
args: SmallVec::new(),
},
)
.expect("log");
logger.close().expect("close");
}
let mut replayer = ProofReplayer::new();
let result = replayer.replay(&path).expect("replay");
assert_eq!(result, VerificationResult::Valid);
assert_eq!(replayer.stats().total_steps, 3);
assert_eq!(replayer.stats().axiom_count, 2);
assert_eq!(replayer.stats().inference_count, 1);
assert_eq!(replayer.stats().total_premises, 2);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_empty_log_is_error() {
let path = temp_path("empty_log");
let _ = std::fs::remove_file(&path);
{
let mut logger = ProofLogger::create(&path).expect("create");
logger.close().expect("close");
}
let result = ProofReplayer::replay_from_file(&path);
assert!(matches!(result, Err(ProofError::EmptyLog)));
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_bad_magic() {
let path = temp_path("bad_magic");
let _ = std::fs::remove_file(&path);
std::fs::write(
&path,
b"BADMAGIC\x01\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00",
)
.expect("write");
let err = ProofReplayer::replay_from_file(&path).expect_err("should fail");
assert!(matches!(err, ProofError::InvalidMagic { .. }));
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_verification_result_display() {
assert_eq!(VerificationResult::Valid.to_string(), "valid");
assert_eq!(
VerificationResult::Invalid("bad step".to_string()).to_string(),
"invalid: bad step"
);
assert_eq!(VerificationResult::Incomplete.to_string(), "incomplete");
}
}