use crate::format::{Chunk, FormatError, RecReader};
use std::collections::BTreeMap;
use std::io::{Read, Seek};
use std::path::Path;
#[derive(Debug)]
pub enum CompareError {
Format(FormatError),
HashAlgoMismatch {
a: u16,
b: u16,
},
NoCommonTicks,
}
impl std::fmt::Display for CompareError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Format(err) => write!(f, "{err}"),
Self::HashAlgoMismatch { a, b } => write!(
f,
"hash algorithm mismatch, id {a} versus id {b}, \
these recordings cannot be compared"
),
Self::NoCommonTicks => write!(
f,
"the recordings share no ticks with hashes on both sides, \
nothing to compare"
),
}
}
}
impl std::error::Error for CompareError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Format(err) => Some(err),
_ => None,
}
}
}
impl From<FormatError> for CompareError {
fn from(err: FormatError) -> Self {
Self::Format(err)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashKind {
Light,
Full,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Divergence {
pub tick: u64,
pub detected_by: HashKind,
pub last_agreeing_tick: Option<u64>,
pub confirming_full_hash_tick: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Identical {
ticks_compared: u64,
extra_ticks_a: u64,
extra_ticks_b: u64,
},
Diverged(Divergence),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompareWarning {
SeedMismatch(u64, u64),
TickRateMismatch(u32, u32),
InputFormatMismatch(u64, u64),
BuildMismatch(String, String),
}
impl std::fmt::Display for CompareWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SeedMismatch(a, b) => write!(
f,
"rng seeds differ, {a:#x} versus {b:#x}, \
different seeds usually mean genuinely different sessions"
),
Self::TickRateMismatch(a, b) => {
write!(f, "tick rates differ, {a} versus {b} ticks per second")
}
Self::InputFormatMismatch(a, b) => write!(
f,
"input format ids differ, {a} versus {b}, \
the inputs may not mean the same thing"
),
Self::BuildMismatch(a, b) => write!(f, "builds differ, {a} versus {b}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompareReport {
pub outcome: Outcome,
pub warnings: Vec<CompareWarning>,
}
impl std::fmt::Display for CompareReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.outcome {
Outcome::Identical {
ticks_compared,
extra_ticks_a,
extra_ticks_b,
} => {
write!(f, "identical over {ticks_compared} compared ticks")?;
if *extra_ticks_a > 0 || *extra_ticks_b > 0 {
write!(
f,
", though coverage differs: {extra_ticks_a} ticks only in the first \
recording, {extra_ticks_b} only in the second"
)?;
}
Ok(())
}
Outcome::Diverged(d) => {
match d.detected_by {
HashKind::Light => {
write!(
f,
"first divergence at tick {}, caught by the light hash",
d.tick
)?;
match d.confirming_full_hash_tick {
Some(t) => write!(f, ", confirmed by the full hash at tick {t}")?,
None => write!(f, ", no full hash available to confirm it")?,
}
}
HashKind::Full => {
write!(
f,
"divergence caught by the full hash at tick {}, while the light \
hash saw nothing: the light hash has a blind spot, and the real \
divergence happened at or before this tick",
d.tick
)?;
}
}
if let Some(t) = d.last_agreeing_tick {
write!(f, ", last agreement at tick {t}")?;
}
Ok(())
}
}
}
}
struct HashTimeline {
light: BTreeMap<u64, u64>,
full: BTreeMap<u64, u64>,
hash_algo_id: u16,
rng_seed: u64,
tick_rate: u32,
input_format_id: u64,
build_hash: String,
}
fn load_timeline<R: Read + Seek>(reader: &mut RecReader<R>) -> Result<HashTimeline, CompareError> {
let header = reader.header().clone();
let mut light = BTreeMap::new();
let mut full = BTreeMap::new();
for item in reader.chunks()? {
match item? {
Chunk::LightHashBatch { first_tick, hashes } => {
for (offset, hash) in hashes.iter().enumerate() {
light.insert(first_tick + offset as u64, *hash);
}
}
Chunk::FullHash { tick, hash } => {
full.insert(tick, hash);
}
_ => {}
}
}
Ok(HashTimeline {
light,
full,
hash_algo_id: header.config.hash_algo_id,
rng_seed: header.meta.rng_seed,
tick_rate: header.meta.tick_rate,
input_format_id: header.config.input_format_id,
build_hash: header.meta.build_hash,
})
}
pub fn first_divergence<A: AsRef<Path>, B: AsRef<Path>>(
a: A,
b: B,
) -> Result<CompareReport, CompareError> {
let mut reader_a = RecReader::open(std::io::BufReader::new(
std::fs::File::open(a).map_err(FormatError::from)?,
))?;
let mut reader_b = RecReader::open(std::io::BufReader::new(
std::fs::File::open(b).map_err(FormatError::from)?,
))?;
first_divergence_from(&mut reader_a, &mut reader_b)
}
pub fn first_divergence_from<Ra: Read + Seek, Rb: Read + Seek>(
a: &mut RecReader<Ra>,
b: &mut RecReader<Rb>,
) -> Result<CompareReport, CompareError> {
let ta = load_timeline(a)?;
let tb = load_timeline(b)?;
if ta.hash_algo_id != tb.hash_algo_id {
return Err(CompareError::HashAlgoMismatch {
a: ta.hash_algo_id,
b: tb.hash_algo_id,
});
}
let mut warnings = Vec::new();
if ta.rng_seed != tb.rng_seed {
warnings.push(CompareWarning::SeedMismatch(ta.rng_seed, tb.rng_seed));
}
if ta.tick_rate != tb.tick_rate {
warnings.push(CompareWarning::TickRateMismatch(ta.tick_rate, tb.tick_rate));
}
if ta.input_format_id != tb.input_format_id {
warnings.push(CompareWarning::InputFormatMismatch(
ta.input_format_id,
tb.input_format_id,
));
}
if ta.build_hash != tb.build_hash {
warnings.push(CompareWarning::BuildMismatch(
ta.build_hash.clone(),
tb.build_hash.clone(),
));
}
let mut ticks_compared: u64 = 0;
let mut last_agreeing: Option<u64> = None;
let mut light_divergence: Option<u64> = None;
for (tick, hash_a) in &ta.light {
if let Some(hash_b) = tb.light.get(tick) {
ticks_compared += 1;
if hash_a == hash_b {
last_agreeing = Some(*tick);
} else {
light_divergence = Some(*tick);
break;
}
}
}
let mut full_divergence: Option<u64> = None;
let mut last_full_agree: Option<u64> = None;
for (tick, hash_a) in &ta.full {
if let Some(hash_b) = tb.full.get(tick) {
if hash_a != hash_b {
full_divergence = Some(*tick);
break;
}
last_full_agree = Some(*tick);
}
}
if ticks_compared == 0 && full_divergence.is_none() {
return Err(CompareError::NoCommonTicks);
}
let outcome = match (light_divergence, full_divergence) {
(None, None) => {
let common = ticks_compared;
Outcome::Identical {
ticks_compared: common,
extra_ticks_a: ta.light.len() as u64 - common,
extra_ticks_b: tb.light.len() as u64 - common,
}
}
(Some(lt), full) => {
let confirming = full.filter(|ft| *ft >= lt);
if let Some(ft) = full
&& ft < lt
{
Outcome::Diverged(Divergence {
tick: ft,
detected_by: HashKind::Full,
last_agreeing_tick: last_full_agree,
confirming_full_hash_tick: Some(ft),
})
} else {
Outcome::Diverged(Divergence {
tick: lt,
detected_by: HashKind::Light,
last_agreeing_tick: last_agreeing,
confirming_full_hash_tick: confirming,
})
}
}
(None, Some(ft)) => Outcome::Diverged(Divergence {
tick: ft,
detected_by: HashKind::Full,
last_agreeing_tick: last_full_agree,
confirming_full_hash_tick: Some(ft),
}),
};
Ok(CompareReport { outcome, warnings })
}